Я, наконец-то, смог снова вернуться к своей разработке. Чувствую, что она "не отпустит" меня, пока не доведу дело до конца.
В этой части я расскажу о довольно значительных изменениях и доработках в схеме процессора. Их уровень уже таков, что процессор выполняет несложные программы; ещё немного - и надо будет писать ассемблер, большие программы в машинных кодах писать очень неудобно.
В прошлой записи я остановился на блоке регистров. Он вполне закончен, к нему возвращаться, пока что, нужды нет.
Следующим логичным шагом стала бы реализация стека. Сделать его оказалось не так уж сложно, но вот когда я попытался с ним работать, возникли затруднения.
Если помните, я решил организовать работу со стеком посредством обычных операндов, вместо того, чтобы использовать стандартные команды работы со стеком PUSH и POP. Такое решение приводило к следующей логике работы: ничего не мешало брать или помещать данные в стек, но изменять их оказалось сложно. Вместо одной операции (и, соответственно, одного такта) получалось три: взять значение, изменить значение, вернуть значение. Хотя текущая схема позволяет оптимизировать порядок исполнения (изменение может производиться одновременно со считыванием либо записью), всё равно это требовало какого-то механизма синхронизации, что было неудобно и усложняло схему.
После некоторых раздумий я отказался от идеи подобной работы со стеком, и решил реализовать классические PUSH и POP.
Это, конечно, лишило меня целых двух опкодов (на самом деле, я думаю обойтись одним), однако такое решение позволило внести упрощения в логику работы процессора и его подсхем. Например, теперь схема определения количества параметров команды OpC сократилась в разы (два десятка элементов против нескольких сотен ранее) - поскольку теперь у каждой команды одно и то же количество параметров независимо от их типа:
Клик для увеличения
Освободившийся номер типа 10b (ранее обозначавший стек) я думаю отдать под операции ввода-вывода в порты (ага, я уже думаю над тем, чтобы пристроить процессору простенький ASCII-терминал и, возможно, клавиатуру).
Соответственно, была переписана таблица опкодов, у подсхем были изменены названия входов. Переделка оказалось несложной.
Поскольку теперь работа со стеком отодвинулась, следующим шагом становилась реализация полноценной работы с памятью. Если помните, контроллер памяти был реализован несколько в черновом варианте: он просто читал память по порядку, парсил содержащиеся в ней команды, но не умел изменять адресацию или менять значения.
Чтобы понять, какие доработки потребуются, перечислим операции, поддержка которых нужна:
1) Переход по адресу (JUMP). Программа выполняется-выполняется, появилась инструкция перехода - произошёл прыжок по указанному адресу, выполнение пошло оттуда.
2) Чтение по адресу (GET). Взять значение по адресу, при этом не совершая перехода.
3) Запись по адресу (SET). Аналогично, только значение записывается.
Одновременное выполнение операций GET и SET (взять значение по одному адресу, записать в другой) вполне реализуемо. Но в архитектуре x86, например, команда MOV [addr1],[addr2] не поддерживается - по той же причине, по которой я отказался от командной работы со стеком: выполнение такой операции займёт минимум два такта и усложнит логику. Поэтому пересылка данных из памяти в память реализуется только через буфер; возможно в других архитектурах однокомандная пересылка реализована, но я и x86 знаю плохо, что уж о говорить других архитектурах.
По этой причине имеем всего три новых операции. Реализуются они вот такой схемой:
Я выкинул псевдоадресный регистр и переделал всё набело. Логика такая: есть три ключа JUMP, GET, SET и восьмибитные контакты: адрес перехода Address, значение записи Set Value, считываемое значение Get Value. Ну и тактовый генератор присоединён к схеме памяти (это нужно для записи). Также был изменён интерфейс схемы памяти: вместо одного синхронного порта ввода/вывода с состоянием, переключаемым сигналом на отдельном входе, использована схема с двумя раздельными портами (это не обязательно, но упрощает конструирование).
Сигнал на JUMP изменяет значение счётчика указателя адреса на значение, считанное со входа Address.
Сигналы на GET или SET отсоединяют счётчик указателя адреса от тактового генератора, вместо этого подавая на адресный вход A схемы памяти адрес из Address. Для GET на этом всё заканчивается - на выходе Get Value всегда значение, заданное по адресу в A. SET же подаёт сигнал на порт str, и соединяет контакт Set Value с портом чтения D. При изменении значения тактового генератора значение обновится, при этом выходы GET/SET должны будут "погаснуть" (поскольку они контролируются "снаружи" схемы, требуется синхронизация, к этому вопросу ещё вернусь), и указатель памяти тут же перескочит на значение из счётчика.
Всё просто. Да, согласующие резисторы нужны поскольку на соответствующих контактах при дальнейшем подключении к процессору могут быть несогласованные значения.
Теоретически, для реализации Тьюринг-полного компьютера уже всё есть с избытком, осталось только запилить командный блок - собственно, ту штуку, которая будет складывать, делить, пересылать и умножать. Но если подумать: командный блок должен бы работать с абсолютными значениями, а не адресами/номерами. То есть сначала абсолютные значения из этих адресов/номеров нужно получить.
В прошлой части я рассказывал, что блок Data selector (DS) не был готов. Сейчас, после того как окончательно (ну я надеюсь, что окончательно) определена структура команд и готова "периферия", вроде блока регистров и контроллера памяти, его уже можно дорабатывать, благо дорабатывать там совершенно нечего. Вот его простая схема:
Семь транзисторов, вот и весь селектор. Логика простая: в зависимости от типа данных подключаем вход чтения к соответствующей схеме, подавая этой схеме на вход требуемый ей параметр (памяти - адрес, регистрам - номер). Выход GET активировать, когда нужно получить значение из памяти.
После того, как становятся известны абсолютные значения параметров, с ними уже можно производить операции. Операций, напомню, на текущий момент, задано шесть: NOP, MOV, XOR, SUM, SHL, SHR.
Реализовать сами эти операции несложно, более того - соответствующая логика уже есть в logisim. Несколько сложнее добиться того, чтобы операция и запись её результата происходили в один такт. Для этого нужно избегать всяких буферных приёмников, и писать сразу по назначению.
Рассмотрим, как реализованы команды MOV и SUM. Эти две команды выбраны потому, что в их реализации есть различие, которое я хотел бы показать. Точнее, отличия есть только у MOV, остальные команды реализованы практически идентично, за исключением базового для них логического преобразования. Да, ещё есть NOP, но его, надеюсь, показывать не нужно =)
Итак, MOV:
Режим работы команды MOV зависит от того, что и куда перемещается. В зависимости от типа приёмника, указанного на одном из входов REG, PORT или MEM, схема подключается к блоку регистров, контроллеру памяти или...
Стоп, стоп, а что с портами? Портов нет, и пока наш процессор беспортовый, контакт PORT присутствует постольку-поскольку. На будущее, так сказать.
Окей, в зависимости от того, идёт пересылка в регистр или память, коммутируем соответствующий выход со входом ADDR (считаем, что на нём либо номер регистра, либо адрес памяти) и подаём на выход значение со входа INPUT VALUE2 (соответствующее второму параметру команды MOV). Почему не первому? Потому что первый параметр - это приёмник, и на значение его плевать. Соответственно, вход INPUT VALUE1 сделан для красоты, не больше.
Вход РАЗРЕШЕНИЕ разрешает передачу такта дальше по схеме. Поскольку и память, и регистры обновляются только при переключении такта, это гарантирует, что без сигнала на этом порту данные не изменятся. Если же вход неактивен, согласующий резистор препятствует возникновению ошибочных значений в схеме, обнуляя выходной байт (который не имеет смысла без передачи такта).
Ну и логический элемент ИЛИ помогает избежать ошибки, когда ни на один из входов REG, PORT или MEM не подано значение. Дело в том, что блок регистра спроектирован так, что при ошибочном значении на входе Write Selector почему-то считает это значение единицей. Я не разбирался, баг это, или фича, но приводит это к тому, что запись происходит во все регистры сразу. Была мыслишка оставить такое поведение как фичу, но до окончательной доработки схемы от мысли этой я отказался.
Теперь SUM:
Отличие очевидно - в схеме используются оба входных значения. А ещё у этой схемы есть выход ПЕРЕНОС, на который подаётся единица, если результат сложения не умещается в байт (он пока есть только у SUM, но некоторые другие команд тоже будут иметь подобный выход).
Все команды я объединил в большой командный блок (далее будет помечен, как CMD), куда также отправилась схема Opcode selector (OS):
Клик для увеличения
Несмотря на обилие проводов, разобраться в ней довольно легко - входы блоков команд параллельно подключены к шине, в один момент выполняется только одна команда, выбранная схемой OS. Неактивные команды подают на все выходы нули, поэтому нужное значение для адреса\номера и данных можно выбрать логическим ИЛИ.
Можно заметить, что выходной такт инвертируется по отношению ко входному. Это нужно для того, чтобы всегда было различие на полтакта между командным блоком (выступающим в роли источника данных) и получателем (блоком регистров или памятью) - тогда получатель сможет обновить значение до того, как оно изменится в командном блоке. Того же результата можно добиться, изменив параметры получателей; logisim позволяет использовать даже не такты, а ключи (например, регистр будет менять значение на поданное не при смене такта, а пока активен сигнальный вход), но это чит. Это не значит, что я совсем не собираюсь этим читом пользоваться - но пока можно без него - буду без него.
Я, в общем-то, не уверен, что командный блок не содержит ошибок, и не будет переделываться в дальнейшем - но это уже зависит от результатов полноценного тестирования процессора в полной, так сказать, сборке.
Окей, все компоненты для запуска готовы, осталось только правильно соединить их - и можно пытаться пускать примитивные программы. Но об этом - в следующей части.
Продолжение следует.
В этой части я расскажу о довольно значительных изменениях и доработках в схеме процессора. Их уровень уже таков, что процессор выполняет несложные программы; ещё немного - и надо будет писать ассемблер, большие программы в машинных кодах писать очень неудобно.
В прошлой записи я остановился на блоке регистров. Он вполне закончен, к нему возвращаться, пока что, нужды нет.
Следующим логичным шагом стала бы реализация стека. Сделать его оказалось не так уж сложно, но вот когда я попытался с ним работать, возникли затруднения.
Если помните, я решил организовать работу со стеком посредством обычных операндов, вместо того, чтобы использовать стандартные команды работы со стеком PUSH и POP. Такое решение приводило к следующей логике работы: ничего не мешало брать или помещать данные в стек, но изменять их оказалось сложно. Вместо одной операции (и, соответственно, одного такта) получалось три: взять значение, изменить значение, вернуть значение. Хотя текущая схема позволяет оптимизировать порядок исполнения (изменение может производиться одновременно со считыванием либо записью), всё равно это требовало какого-то механизма синхронизации, что было неудобно и усложняло схему.
После некоторых раздумий я отказался от идеи подобной работы со стеком, и решил реализовать классические PUSH и POP.
Это, конечно, лишило меня целых двух опкодов (на самом деле, я думаю обойтись одним), однако такое решение позволило внести упрощения в логику работы процессора и его подсхем. Например, теперь схема определения количества параметров команды OpC сократилась в разы (два десятка элементов против нескольких сотен ранее) - поскольку теперь у каждой команды одно и то же количество параметров независимо от их типа:
Клик для увеличения
Освободившийся номер типа 10b (ранее обозначавший стек) я думаю отдать под операции ввода-вывода в порты (ага, я уже думаю над тем, чтобы пристроить процессору простенький ASCII-терминал и, возможно, клавиатуру).
Соответственно, была переписана таблица опкодов, у подсхем были изменены названия входов. Переделка оказалось несложной.
Поскольку теперь работа со стеком отодвинулась, следующим шагом становилась реализация полноценной работы с памятью. Если помните, контроллер памяти был реализован несколько в черновом варианте: он просто читал память по порядку, парсил содержащиеся в ней команды, но не умел изменять адресацию или менять значения.
Чтобы понять, какие доработки потребуются, перечислим операции, поддержка которых нужна:
1) Переход по адресу (JUMP). Программа выполняется-выполняется, появилась инструкция перехода - произошёл прыжок по указанному адресу, выполнение пошло оттуда.
2) Чтение по адресу (GET). Взять значение по адресу, при этом не совершая перехода.
3) Запись по адресу (SET). Аналогично, только значение записывается.
Одновременное выполнение операций GET и SET (взять значение по одному адресу, записать в другой) вполне реализуемо. Но в архитектуре x86, например, команда MOV [addr1],[addr2] не поддерживается - по той же причине, по которой я отказался от командной работы со стеком: выполнение такой операции займёт минимум два такта и усложнит логику. Поэтому пересылка данных из памяти в память реализуется только через буфер; возможно в других архитектурах однокомандная пересылка реализована, но я и x86 знаю плохо, что уж о говорить других архитектурах.
По этой причине имеем всего три новых операции. Реализуются они вот такой схемой:
Я выкинул псевдоадресный регистр и переделал всё набело. Логика такая: есть три ключа JUMP, GET, SET и восьмибитные контакты: адрес перехода Address, значение записи Set Value, считываемое значение Get Value. Ну и тактовый генератор присоединён к схеме памяти (это нужно для записи). Также был изменён интерфейс схемы памяти: вместо одного синхронного порта ввода/вывода с состоянием, переключаемым сигналом на отдельном входе, использована схема с двумя раздельными портами (это не обязательно, но упрощает конструирование).
Сигнал на JUMP изменяет значение счётчика указателя адреса на значение, считанное со входа Address.
Сигналы на GET или SET отсоединяют счётчик указателя адреса от тактового генератора, вместо этого подавая на адресный вход A схемы памяти адрес из Address. Для GET на этом всё заканчивается - на выходе Get Value всегда значение, заданное по адресу в A. SET же подаёт сигнал на порт str, и соединяет контакт Set Value с портом чтения D. При изменении значения тактового генератора значение обновится, при этом выходы GET/SET должны будут "погаснуть" (поскольку они контролируются "снаружи" схемы, требуется синхронизация, к этому вопросу ещё вернусь), и указатель памяти тут же перескочит на значение из счётчика.
Всё просто. Да, согласующие резисторы нужны поскольку на соответствующих контактах при дальнейшем подключении к процессору могут быть несогласованные значения.
Теоретически, для реализации Тьюринг-полного компьютера уже всё есть с избытком, осталось только запилить командный блок - собственно, ту штуку, которая будет складывать, делить, пересылать и умножать. Но если подумать: командный блок должен бы работать с абсолютными значениями, а не адресами/номерами. То есть сначала абсолютные значения из этих адресов/номеров нужно получить.
В прошлой части я рассказывал, что блок Data selector (DS) не был готов. Сейчас, после того как окончательно (ну я надеюсь, что окончательно) определена структура команд и готова "периферия", вроде блока регистров и контроллера памяти, его уже можно дорабатывать, благо дорабатывать там совершенно нечего. Вот его простая схема:
Семь транзисторов, вот и весь селектор. Логика простая: в зависимости от типа данных подключаем вход чтения к соответствующей схеме, подавая этой схеме на вход требуемый ей параметр (памяти - адрес, регистрам - номер). Выход GET активировать, когда нужно получить значение из памяти.
После того, как становятся известны абсолютные значения параметров, с ними уже можно производить операции. Операций, напомню, на текущий момент, задано шесть: NOP, MOV, XOR, SUM, SHL, SHR.
Реализовать сами эти операции несложно, более того - соответствующая логика уже есть в logisim. Несколько сложнее добиться того, чтобы операция и запись её результата происходили в один такт. Для этого нужно избегать всяких буферных приёмников, и писать сразу по назначению.
Рассмотрим, как реализованы команды MOV и SUM. Эти две команды выбраны потому, что в их реализации есть различие, которое я хотел бы показать. Точнее, отличия есть только у MOV, остальные команды реализованы практически идентично, за исключением базового для них логического преобразования. Да, ещё есть NOP, но его, надеюсь, показывать не нужно =)
Итак, MOV:
Режим работы команды MOV зависит от того, что и куда перемещается. В зависимости от типа приёмника, указанного на одном из входов REG, PORT или MEM, схема подключается к блоку регистров, контроллеру памяти или...
Стоп, стоп, а что с портами? Портов нет, и пока наш процессор беспортовый, контакт PORT присутствует постольку-поскольку. На будущее, так сказать.
Окей, в зависимости от того, идёт пересылка в регистр или память, коммутируем соответствующий выход со входом ADDR (считаем, что на нём либо номер регистра, либо адрес памяти) и подаём на выход значение со входа INPUT VALUE2 (соответствующее второму параметру команды MOV). Почему не первому? Потому что первый параметр - это приёмник, и на значение его плевать. Соответственно, вход INPUT VALUE1 сделан для красоты, не больше.
Вход РАЗРЕШЕНИЕ разрешает передачу такта дальше по схеме. Поскольку и память, и регистры обновляются только при переключении такта, это гарантирует, что без сигнала на этом порту данные не изменятся. Если же вход неактивен, согласующий резистор препятствует возникновению ошибочных значений в схеме, обнуляя выходной байт (который не имеет смысла без передачи такта).
Ну и логический элемент ИЛИ помогает избежать ошибки, когда ни на один из входов REG, PORT или MEM не подано значение. Дело в том, что блок регистра спроектирован так, что при ошибочном значении на входе Write Selector почему-то считает это значение единицей. Я не разбирался, баг это, или фича, но приводит это к тому, что запись происходит во все регистры сразу. Была мыслишка оставить такое поведение как фичу, но до окончательной доработки схемы от мысли этой я отказался.
Теперь SUM:
Отличие очевидно - в схеме используются оба входных значения. А ещё у этой схемы есть выход ПЕРЕНОС, на который подаётся единица, если результат сложения не умещается в байт (он пока есть только у SUM, но некоторые другие команд тоже будут иметь подобный выход).
Все команды я объединил в большой командный блок (далее будет помечен, как CMD), куда также отправилась схема Opcode selector (OS):
Клик для увеличения
Несмотря на обилие проводов, разобраться в ней довольно легко - входы блоков команд параллельно подключены к шине, в один момент выполняется только одна команда, выбранная схемой OS. Неактивные команды подают на все выходы нули, поэтому нужное значение для адреса\номера и данных можно выбрать логическим ИЛИ.
Можно заметить, что выходной такт инвертируется по отношению ко входному. Это нужно для того, чтобы всегда было различие на полтакта между командным блоком (выступающим в роли источника данных) и получателем (блоком регистров или памятью) - тогда получатель сможет обновить значение до того, как оно изменится в командном блоке. Того же результата можно добиться, изменив параметры получателей; logisim позволяет использовать даже не такты, а ключи (например, регистр будет менять значение на поданное не при смене такта, а пока активен сигнальный вход), но это чит. Это не значит, что я совсем не собираюсь этим читом пользоваться - но пока можно без него - буду без него.
Я, в общем-то, не уверен, что командный блок не содержит ошибок, и не будет переделываться в дальнейшем - но это уже зависит от результатов полноценного тестирования процессора в полной, так сказать, сборке.
Окей, все компоненты для запуска готовы, осталось только правильно соединить их - и можно пытаться пускать примитивные программы. Но об этом - в следующей части.
Продолжение следует.
Комментариев нет:
Отправить комментарий