3.3 Реализация Триггерные вентили DFF (Триггер данных) могут быть реализованы из логических вентилей более низкого уровня, подобных тем, которые были созданы в главе 1. Однако в этой книге мы рассматриваем DFF как примитивные вентили, и, таким образом, они могут использоваться в проектах построения аппаратных средств, не беспокоясь об их внутренней реализации. Рисунок 3.5 Счетчик симуляции. В момент 23 выдается сигнал сброса, в результате чего счетчик показывает 0 в следующую единицу времени. Значение 0 сохраняется до тех пор, пока не будет выдан сигнал вкл. в момент времени 25, в результате чего счетчик начнет увеличиваться, на единицу времени позже. Отсчет продолжается до тех пор, пока в момент 29 не будет установлен бит загрузки. Поскольку на входе счетчика находится число 527, счетчик сбрасывается на это значение в следующую единицу времени. Поскольку вкл. все еще утверждается, счетчик продолжает увеличиваться до момента 33, когда вкл. отменяется. 1-битный регистр (бит) Реализация этого чипа была представлена на рисунке 3.1. Регистр. Конструкция микросхемы W-битового регистра из 1-битных регистров проста. Все, что нам нужно сделать, - это создать массив w-битных вентилей и направить входную нагрузку регистра каждому из них. 8-регистровая память (RAM8). Проверка рисунка 3.3 может быть полезна здесь. Для реализации чипа RAM8 мы выстроили массив из восьми регистров. Затем мы должны создать комбинационную логику, которая, учитывая определенное значение адреса, принимает данные RAM8 и загружает их в выбранный регистр. Аналогичным образом, мы должны создать комбинационную логику, которая при заданном значении адреса выбирает правильный регистр и передает его значение на выходе на выходной сигнал RAM8. Совет: эта комбинационная логика уже была реализована в главе 1. n-регистр памяти. Банк памяти произвольной длины (степень 2) может быть построен рекурсивно из меньших блоков памяти, вплоть до уровня одного регистра. Этот вид изображен на рисунке 3.6. Сосредоточив внимание на правой части рисунка, отметим, что 64-разрядная ОЗУ может быть построена из массива из восьми 8-разрядных ОЗУ. Чтобы выбрать конкретный регистр из памяти RAM64, мы используем 6-битный адрес, скажем, xxxyyy. Биты MSB xxx выбирают один из чипов RAM8, а биты LSB yyy выбирают один из регистров в выбранном RAM8. Чип RAM64 должен быть оборудован логическими схемами, которые влияют на эту иерархическую схему адресации. Счетчик. W-битный счетчик состоит из двух основных элементов: обычного W-битного регистра и комбинационной логики. Комбинационная логика предназначена для (а) вычисления функции счета и (б) перевода счетчика в правильный режим работы, в соответствии со значениями его трех управляющих битов. Совет: Большая часть этой логики уже была построена в главе 2. 3.4 Перспектива Краеугольным камнем всех систем памяти, описанных в этой главе, является триггер - вентили, которые мы здесь рассматриваем как атомарный или примитивный строительный блок. Обычный подход в аппаратных учебниках состоит в том, чтобы создавать триггеры из элементарных комбинаторных вентилей (например, вентилей Нанда) с использованием соответствующих петель обратной связи. Стандартная конструкция начинается с построения простого (не синхронизированного) триггера, который является бистабильным, а именно, который может быть установлен в одно из двух состояний. Затем получается синхронизированный триггер путем каскадирования двух таких простых триггеров, первый из которых устанавливается, когда часы тикают, и второй, когда часы взлетают. Этот «ведущий-ведомый» дизайн наделяет весь триггер желаемой функциональностью синхронизированной синхронизации. Рисунок 3.6. Постепенное построение банков памяти путем рекурсивного всплытия. W-битный регистр представляет собой массив из w двоичных ячеек, 8-регистровая RAM - это массив из восьми w-битных регистров, 64-регистровая RAM - это массив из восьми чипов RAM8 и так далее. Для построения чипа ОЗУ объемом 16 КБ требуется всего лишь три аналогичных этапа. Эти конструкции довольно сложны, требуют понимания деликатных вопросов, таких как влияние петель обратной связи на комбинаторные схемы, а также реализация тактов с использованием двухфазного двоичного тактового сигнала. В этой книге мы решили абстрагироваться от этих низкоуровневых соображений, рассматривая триггер как атомные врата. Читатели, желающие изучить внутреннюю структуру вьетнамок, могут найти подробные описания в большинстве учебников по логическому проектировщику и компьютерной архитектуре. В заключение отметим, что устройства памяти современных компьютеров не всегда построены из стандартных триггеров. Вместо этого современные чипы памяти обычно очень тщательно оптимизируются, используя уникальные физические свойства лежащей в основе технологии хранения. Многие такие альтернативные технологии сегодня доступны для компьютерных разработчиков, как обычно, какую технологию использовать - это вопрос эффективности затрат. Помимо этих низкоуровневых соображений, все остальные конструкции микросхем в этой главе - регистры и микросхемы памяти, которые были построены поверх триггеров - были стандартными. 3.5 Проект Задача. Соберите все чипы, описанные в главе. Единственными строительными блоками, которые вы можете использовать, являются примитивные ворота DFF (триггеры памяти), чипы, которые вы будете строить поверх них, и чипы, описанные в предыдущих главах. Ресурсы. Единственный инструмент, который вам нужен для этого проекта, это аппаратный симулятор, поставляемый с книгой. Все чипы должны быть реализованы на языке HDL, указанном в приложении A. Как обычно, для каждого чипа мы поставляем скелетную программу .hdl с отсутствующей частью реализации, файл сценария .tst, который сообщает аппаратному симулятору, как его тестировать, и Файл сравнения .cmp. Ваша задача - заполнить недостающие части реализации поставляемых .hdl программ. Договор. При загрузке в аппаратный симулятор конструкция вашего чипа (модифицированная программа .hdl), протестированная на прилагаемом файле .tst, должна давать выходные данные, перечисленные в прилагаемом файле .cmp. Если это не так, симулятор сообщит вам. Совет. Гейт Data Flip-Flop (DFF) считается примитивным, и поэтому нет необходимости его строить: когда симулятор встречает гейт DFF в программе HDL, он автоматически вызывает реализацию встроенных tools / builtIn / DFF.hdl. Структура каталогов этого проекта. При изготовлении чипов оперативной памяти из более мелких мы рекомендуем использовать встроенные версии последних. В противном случае симулятор может работать очень медленно или даже из (реальной) области памяти, поскольку большие микросхемы ОЗУ содержат десятки тысяч микросхем более низкого уровня, и все эти микросхемы хранятся в памяти (как программные объекты) имитатором. По этой причине мы поместили программы RAM512.hdl, RAM4K.hdl и RAM16K.hdl в отдельный каталог. Таким образом, конструкция рекурсивного спуска микросхем RAM4K и RAM16K останавливается на микросхеме RAM512, тогда как микросхемы более низкого уровня, из которых сделан последний чип, обязательно являются встроенными (поскольку симулятор не находит их в этом каталоге ). Шаги. Мы рекомендуем действовать в следующем порядке: 0. Аппаратный симулятор, необходимый для этого проекта, доступен в каталоге инструментов программного пакета книги. 1. Прочитайте приложение A, сосредоточив внимание на разделах A.6 и A.7. 2. Пройдите учебник по аппаратному симулятору, сосредотачиваясь на частях IV и V. 3. Постройте и смоделируйте все микросхемы, указанные в каталоге projects / 03. 4. Машинный язык Все должно быть сделано так просто, как только возможно, но не проще. Альберт Эйнштейн (1879-1955) Компьютер можно описать конструктивно, изложив его аппаратную платформу и объяснив, как он построен из низкоуровневых микросхем. Компьютер также можно описать абстрактно, указав и продемонстрировав его возможности машинного языка. И действительно, удобно знакомиться с новой компьютерной системой, сначала посмотрев некоторые низкоуровневые программы, написанные на ее машинном языке. Это помогает нам понять не только то, как программировать компьютер, чтобы он делал полезные вещи, но также и то, почему его оборудование было разработано определенным образом. Учитывая это, в этой главе основное внимание уделяется низкоуровневому программированию на машинном языке. Это готовит почву для главы 5, где мы заканчиваем конструирование компьютера общего назначения, предназначенного для запуска программ на машинном языке. Этот компьютер будет построен из чипсета, встроенного в главы 13. Машинный язык - это согласованный формализм, разработанный для кодирования низкоуровневых программ в виде серии машинных инструкций. Используя эти инструкции, программист может дать команду процессору выполнять арифметические и логические операции, извлекать и сохранять значения из памяти и в нее, перемещать значения из одного регистра в другой, тестировать логические условия и т. Д. В отличие от языков высокого уровня, основными целями проектирования которых являются универсальность и мощь выражения, целью проектирования машинного языка является непосредственное выполнение и полный контроль над данной аппаратной платформой. Конечно, универсальность, мощь и элегантность по-прежнему желательны, но только в той степени, в которой они поддерживают основное требование прямого исполнения в оборудовании. Машинный язык - самый глубокий интерфейс в компьютерном бизнесе - тонкая грань, где встречаются аппаратное и программное обеспечение. В этот момент абстрактные мысли программиста, выраженные в символических инструкциях, превращаются в физические операции, выполняемые в кремнии. Таким образом, машинный язык может быть истолкован как инструмент программирования и неотъемлемая часть аппаратной платформы. Фактически, точно так же, как мы говорим, что машинный язык предназначен для использования определенной аппаратной платформы, мы можем сказать, что аппаратная платформа предназначена для извлечения, интерпретации и выполнения инструкций, написанных на данном машинном языке. Глава начинается с общего введения в программирование на машинном языке. Далее мы даем подробную спецификацию машинного языка Hack, охватывающую как его двоичную, так и его символьную версии сборки. Проект, который завершает главу, вовлекает вас в написание пары программ на машинном языке. Этот проект предлагает практическую оценку низкоуровневого программирования и готовит вас к созданию самого компьютера в следующей главе. Хотя большинство людей никогда не будут писать программы непосредственно на машинном языке, изучение низкоуровневого программирования является необходимым условием для полного понимания компьютерных архитектур. Кроме того, довольно увлекательно осознавать, как самые сложные программные системы представляют собой длинный набор элементарных инструкций, каждая из которых определяет очень простую и примитивную операцию на базовом оборудовании. Как обычно, это понимание лучше всего достичь конструктивно, написав некоторый низкоуровневый код и запустив его непосредственно на аппаратной платформе. 4.1 История вопроса Эта глава ориентирована на язык. Поэтому мы можем абстрагироваться от большинства деталей базовой аппаратной платформы, перенеся ее описание в следующую главу. Действительно, чтобы дать общее описание машинных языков, достаточно сосредоточиться только на трех основных положениях: процессор, память и набор регистров. 4.1.1 Машины Машинный язык можно рассматривать как согласованный формализм, предназначенный для манипулирования памятью с использованием процессора и набора регистров. Память. Термин «память» относится к совокупности аппаратных устройств, которые хранят данные и инструкции на компьютере. С точки зрения программиста, все воспоминания имеют одинаковую структуру: непрерывный массив ячеек фиксированной ширины, также называемых словами или местоположениями, каждый из которых имеет уникальный адрес. Следовательно, отдельное слово (представляющее либо элемент данных, либо инструкцию) указывается путем предоставления его адреса. В дальнейшем мы будем ссылаться на такие отдельные слова, используя эквивалентные обозначения Memory [address], RAM [address] или M [address] для краткости. Процессор. Процессор, обычно называемый центральным процессором или ЦП, представляет собой устройство, способное выполнять фиксированный набор элементарных операций. Как правило, они включают в себя арифметические и логические операции, операции доступа к памяти и управление (также называемые операциями ветвления. Операндами этих операций являются двоичные значения, которые поступают из регистров и выбранных ячеек памяти. Аналогично, результаты операций (выходные данные процессора) могут храниться либо в регистрах, либо в выбранных ячейках памяти. Регистры. Доступ к памяти является относительно медленной операцией, требующей длинных форматов команд (для адреса может потребоваться 32 бита). По этой причине большинство процессоров оснащены несколькими регистрами, каждый из которых может содержать одно значение. Расположенные в непосредственной близости от процессора, регистры служат высокоскоростной локальной памятью, что позволяет процессору быстро манипулировать данными и инструкциями. Этот параметр позволяет программисту минимизировать использование команд доступа к памяти, тем самым ускоряя выполнение программы. 4.1.2 Языки Программа машинного языка - это серия закодированных инструкций. Например, типичная инструкция в 16-битном компьютере может быть 1010001100011001. Чтобы выяснить, что означает эта инструкция, мы должны знать правила игры, а именно набор команд базовой аппаратной платформы. Например, язык может быть таким, что каждая инструкция состоит из четырех 4битных полей: крайнее левое поле кодирует операцию ЦП, а остальные три поля представляют операнды операции. Таким образом, предыдущая команда может кодировать набор операций R3 в R1 + R9, в зависимости, конечно, от спецификации оборудования и синтаксиса машинного языка. Поскольку двоичные коды довольно загадочны, машинные языки обычно задаются с использованием как двоичных кодов, так и символической мнемоники (мнемоника - это символическая метка, имя которой намекает на то, что она обозначает - в нашем случае аппаратные элементы и двоичные операции). Например, разработчик языка может решить, что код операции 1010 будет представлен мнемоническим сложением и что регистры машины будут символически ссылаться с использованием символов R0, R1, R2 и так далее. Используя эти соглашения, можно указывать инструкции машинного языка либо напрямую, например 1010001100011001, либо символически, например, ADD R3, R1, R9. Делая эту символическую абстракцию на шаг дальше, мы можем позволить себе не только читать символьную запись, но и фактически писать программы, используя символические команды, а не двоичные инструкции. Затем мы можем использовать программу обработки текста, чтобы анализировать символические команды в их базовые поля (мнемонику и операнды), переводить каждое поле в его эквивалентное двоичное представление и собирать полученные коды в двоичные машинные инструкции. Символическая запись называется ассемблером, или просто ассемблером, а программа, которая переводится из ассемблера в двоичный файл, называется ассемблером. Поскольку разные компьютеры различаются с точки зрения операций ЦП, количества и типа регистров и правил синтаксиса сборки, существует Вавилонская башня машинных языков, каждый из которых имеет свой непонятный синтаксис. Тем не менее, независимо от этого разнообразия, все машинные языки поддерживают одинаковые наборы общих команд, которые мы сейчас опишем. 4.1.3 Команды Арифметические и логические операции Каждый компьютер должен выполнять основные арифметические операции, такие как сложение и вычитание, а также базовые логические операции, такие как побитовое отрицание, сдвиг битов и так далее. Вот несколько примеров, написанных в типичном синтаксисе машинного языка: Доступ к памяти. Команды доступа к памяти делятся на две категории. Во-первых, как мы только что видели, арифметические и логические команды могут работать не только с регистрами, но и с выбранными ячейками памяти. Во-вторых, все компьютеры имеют явные команды загрузки и хранения, предназначенные для перемещения данных между регистрами и памятью. Эти команды доступа к памяти могут использовать несколько типов режимов адресации - способы указания адреса требуемого слова памяти. Как обычно, разные компьютеры предлагают разные возможности и разные нотации, но почти всегда поддерживаются следующие три режима доступа к памяти: ■ Прямая адресация. Наиболее распространенный способ обращения к памяти - это указать конкретный адрес или использовать символ, который относится к конкретному адресу, следующим образом: ■ Немедленная адресация. Эта форма адресации используется для загрузки констант, а именно для загрузки значений, которые появляются в коде инструкции: вместо того, чтобы рассматривать числовое поле, которое появляется в инструкции, как адрес, мы просто загружаем значение самого поля в регистр, как следующим образом: ■ Косвенная адресация. В этом режиме адресации адрес требуемой ячейки памяти не жестко закодирован в инструкции; вместо этого инструкция указывает область памяти, которая содержит требуемый адрес. Этот режим адресации используется для обработки указателей. Например, рассмотрим команду высокого уровня x = foo [j], где foo - переменная массива, а x и j целочисленные переменные. Какой машинный язык эквивалентен этой команде? Хорошо, когда массив foo объявляется и инициализируется в программе высокого уровня, компилятор выделяет сегмент памяти для хранения данных массива и заставляет символ foo ссылаться на базовый адрес этого сегмента. Теперь, когда компилятор позже встречает ссылки на ячейки массива, такие как foo [j] (foo метапеременная, для обозначения неопределённого (пока) объекта: функции, процесса, и т. п), он переводит их следующим образом. Во-первых, обратите внимание, что запись j-го массива должна физически находиться в ячейке памяти, которая находится на смещении j от базового адреса массива (для простоты предполагается, что каждый элемент массива использует одно слово). Следовательно, адрес, соответствующий выражению foo [j], может быть легко вычислен путем добавления значения j к значению foo. Так, например, в языке программирования C такая команда, как x = foo [j], также может быть выражена как x = * (foo + j), где обозначение «* n» обозначает «значение Memory [n]». ». При переводе на машинный язык такие команды обычно генерируют следующий код (в зависимости от синтаксиса языка ассемблера): Плавность управления. В то время как программы обычно выполняются линейно, одна команда за другой, они также включают случайные переходы в места, отличные от следующей команды. Ветвление служит нескольким целям, включая повторение (переход назад к началу цикла), условное выполнение (если логическое условие ложно, переход вперед к месту после предложения if-then) и вызов подпрограммы (переход к первому команда некоторого другого сегмента кода). Чтобы поддерживать эти программные конструкции, каждый машинный язык имеет средства для перехода к выбранным местам в программе, как условно, так и безусловно. В языках ассемблера местоположениям в программе также могут быть даны символические имена, используя некоторый синтаксис для указания меток. Рисунок 4.1 иллюстрирует типичный пример. Рисунок 4.1. Логика ветвления высокого и низкого уровня. Синтаксис команд goto варьируется от одного языка к другому, но основная идея та же. Команды безусловного перехода, такие как JMP beginWhile, указывают только адрес целевого местоположения. Команды условного перехода, такие как JNG R1, endWhile, также должны указывать логическое условие, выраженное некоторым образом. В некоторых языках условие является явной частью команды, в то время как в других это побочный продукт выполнения предыдущей команды. На этом заканчивается наше неформальное знакомство с машинными языками и общими операциями, которые они обычно поддерживают. В следующем разделе дается формальное описание одного конкретного машинного языка - нативный код компьютера, который мы создадим в главе 5. 4.2 Описание машинного языка Hack 4.2.1 Обзор Hack (это язык программирования для виртуальной машины HipHop (HHVM), созданный Facebook как диалект PHP ) компьютер - это платформа фон Неймана. Это 16-разрядный компьютер, состоящий из ЦП, двух отдельных модулей памяти, служащих в качестве памяти команд и памяти данных, и двух устройств ввода-вывода с отображением в памяти: экрана и клавиатуры. Адресные пространства памяти. Программист Hack знает о двух различных адресных пространствах: память команд и память данных. Обе памяти имеют 16-битную ширину и 15битное адресное пространство, а это означает, что максимальный адресуемый размер каждой памяти составляет 32K 16-битных слов. ЦП может выполнять только те программы, которые находятся в памяти команд. Память команд является устройством только для чтения, и в него загружаются программы с использованием некоторых внешних средств. Например, память команд может быть реализована в микросхеме ПЗУ, которая предварительно записана с необходимой программой. Загрузка новой программы осуществляется путем замены всего чипа ПЗУ, аналогично замене картриджа в игровой консоли. Для того, чтобы смоделировать эту операцию, аппаратные симуляторы платформы Hack должны предоставить средство для загрузки памяти команд из текстового файла, содержащего программу на машинном языке. Регистры. Программист Hack знает о двух 16-битных регистрах, называемых D и A. Этими регистрами можно явно манипулировать с помощью арифметических и логических инструкций, таких как A = D-1 или D =! A (где «!» Означает 16-битную операцию Not ). В то время как D используется исключительно для хранения значений данных, A служит как регистром данных, так и регистром адресов. То есть, в зависимости от контекста инструкции, содержимое A может интерпретироваться либо как значение данных, либо как адрес в памяти данных, либо как адрес в памяти команд, как мы сейчас объясним. Во-первых, регистр A может использоваться для облегчения прямого доступа к памяти данных (которая отныне будет часто называться «памятью»). Поскольку инструкции Hack имеют ширину 16 бит и адреса задаются с использованием 15 бит, невозможно объединить код операции и адрес в одну инструкцию. Таким образом, синтаксис языка Hack предписывает, чтобы инструкции доступа к памяти работали в неявной ячейке памяти, помеченной «M», например, D = M + 1. Чтобы разрешить этот адрес, соглашение состоит в том, что M всегда ссылается на слово памяти, адрес которого является текущим значением регистра A. Например, если мы хотим выполнить операцию D = Memory [516] - 1, мы должны использовать одну инструкцию, чтобы установить регистр A на 516, и последующую инструкцию, чтобы указать D = M-1. Кроме того, трудолюбивый регистр A также используется для облегчения прямого доступа к памяти команд. Подобно соглашению о доступе к памяти, инструкции Hack jump не указывают конкретный адрес. Вместо этого существует соглашение, что любая операция перехода всегда производит переход к инструкции, находящейся в слове памяти, адресуемой буквой A. Таким образом, если мы хотим выполнить операцию, переходите к 35, мы используем одну инструкцию, чтобы установить A в 35, а вторую - инструкция для кодирования команды goto без указания адреса. Эта последовательность заставляет компьютер извлекать инструкцию, расположенную в InstructionMemory [35], в следующем тактовом цикле. Пример. Поскольку язык Hack не требует пояснений, мы начнем с примера. Единственная неочевидная команда в языке - это @value, где значение - это либо число, либо символ, представляющий число. Эта команда просто сохраняет указанное значение в регистре А. Например, если сумма относится к ячейке памяти 17, то и @ 17, и @sum будут иметь одинаковый эффект: A ← 17. А теперь к примеру: предположим, что мы хотим добавить целые числа от 1 до 100, используя повторное сложение. Рисунок 4.2 дает решение на языке C и возможную компиляцию на язык Hack. Хотя синтаксис Hack более доступен, чем синтаксис большинства машинных языков, он все равно может показаться неясным читателям, не знакомым с низкоуровневым программированием. В частности, обратите внимание, что для каждой операции, связанной с определением места в памяти, требуются две команды Hack: одна для выбора адреса, по которому мы хотим работать, и одна для указания требуемой операции. Действительно, язык Hack состоит из двух общих инструкций: адресной инструкции, также называемой A-инструкцией, и вычислительной инструкции, также называемой C -инструкцией. Каждая инструкция имеет двоичное представление, символическое представление и влияние на компьютер, как мы сейчас указываем. 4.2.2 А-инструкция A-инструкция используется для установки регистра A в 15-битное значение: Эта инструкция заставляет компьютер сохранять указанное значение в регистре А. Например, инструкция @ 5, которая эквивалентна 0000000000000101, заставляет компьютер сохранять двоичное представление 5 в регистре A. А-инструкция используется для трех разных целей. Во-первых, это единственный способ ввести константу в компьютер под управлением программы. Во-вторых, он устанавливает стадию для последующей C-инструкции, предназначенной для манипулирования определенной ячейкой памяти данных, сначала устанавливая A в адрес этой ячейки. В-третьих, он устанавливает стадию для следующей C-инструкции, которая задает переход, сначала загружая адрес назначения перехода в регистр A. Эти виды использования продемонстрированы на рисунке 4.2. Рисунок 4.2 C и версии сборки одной и той же программы. Бесконечный цикл в конце программы является нашим стандартным способом «прекратить» выполнение программ Hack. 4.2.3. C-инструкция C-инструкция - рабочая лошадка программирования платформы Hack - инструкция, которая выполняет почти все. Код инструкции является спецификацией, которая отвечает на три вопроса: (а) что вычислять, (б) где хранить вычисленное значение и (в) что делать дальше? Наряду с Аинструкцией эти спецификации определяют все возможные операции компьютера. Самый левый бит - это код C-инструкции, который равен 1. Следующие два бита не используются. Остальные биты образуют три поля, которые соответствуют трем частям символического представления инструкции. Общая семантика символической инструкции dest = comp; jump выглядит следующим образом. Поле comp указывает ALU, что вычислять. Поле dest указывает, где хранить вычисленное значение (вывод ALU). Поле перехода определяет условие перехода, а именно, какую команду выбрать и выполнить следующей. Теперь опишем формат и семантику каждого из трех полей. Описание вычислений. Hack АЛУ (ариф-логич устройство) предназначен для вычисления фиксированного набора функций в регистрах D, A и M (где M обозначает Память [A]). Вычисляемая функция определяется битом a и шестью битами c, составляющими поле comp инструкции. Этот 7-битный шаблон может потенциально кодировать 128 различных функций, из которых только 28, перечисленные на рисунке 4.3, документированы в спецификации языка. Напомним, что формат C-инструкции - 111a cccc ccdd djjj. Предположим, что мы хотим, чтобы ALU вычислял D-1, текущее значение регистра D минус 1. В соответствии с рисунком 4.3 это можно сделать, введя команду 1110 0011 1000 0000 (7-битный код операции выделен жирным шрифтом) , Чтобы вычислить значение D | M, мы выдаем инструкцию 1111 0101 0100 0000. Чтобы вычислить константу-1, мы выдаем инструкцию 1110 1110 1000 0000 и так далее. Описание назначения. Значение, вычисленное компонентом comp C-инструкции, может храниться в нескольких местах назначения, как указано в 3-битной части dest команды (см. Рисунок 4.4). Первый и второй d-бит кодируют, сохранять ли вычисленное значение в регистре A и в регистре D соответственно. Третий d-бит кодирует, сохранять ли вычисленное значение в M (т.е. в памяти [A]). Один, более одного или ни один из этих битов не может быть установлен. Рисунок 4.3. Вычислительное поле C-инструкции. D и A - это имена регистров. M относится к ячейке памяти, адресуемой A, а именно к памяти [A]. Символы + и - обозначают сложение и вычитание 16-битного 2, а!, | И & - 16-битные логические операторы Not, Or и And соответственно. Обратите внимание на сходство между этим набором команд и спецификацией ALU, приведенной на рисунке 2.6. Напомним, что формат C-инструкции - 111a cccc ccdd djjj. Предположим, мы хотим, чтобы компьютер увеличил значение Memory [7] на 1, а также сохранил результат в регистре D. В соответствии с рисунками 4.3 и 4.4 это можно выполнить с помощью следующих инструкций: Рисунок 4.4. Поле объекта C-инструкции. Первая инструкция заставляет компьютер выбрать регистр памяти, адрес которого равен 7 (так называемый регистр М). Вторая инструкция вычисляет значение M + 1 и сохраняет результат как в M, так и в D. Описание перехода. Поле перехода C-инструкции указывает компьютеру, что делать дальше. Есть две возможности: компьютер должен либо извлечь и выполнить следующую инструкцию в программе, которая используется по умолчанию, либо он должен извлечь и выполнить инструкцию, расположенную в другом месте программы. В последнем случае мы предполагаем, что регистр A был предварительно настроен на адрес, по которому мы должны перейти. Должен ли в действительности произойти скачок, зависит от трех j-битов поля скачка и от выходного значения ALU (вычисленного в соответствии с полем comp). Первый j-бит указывает, следует ли перейти в случае, если это значение отрицательное, второй j-бит в случае, если значение равно нулю, и третий j-бит в случае, если оно положительное. Это дает восемь возможных условий прыжка, показанных на рисунке 4.5. В следующем примере показаны команды перехода в действии: Рисунок 4.5 Поле перехода C-инструкции. Out относится к выводу ALU (полученному из части comp инструкции), а переход подразумевает «продолжить выполнение с инструкцией, адресуемой регистром A». Последняя инструкция (0; JMP) производит безусловный переход. Поскольку синтаксис Cинструкции требует, чтобы мы всегда выполняли некоторые вычисления, мы инструктируем ALU вычислять 0 (произвольный выбор), что игнорируется. Конфликтующее использование регистра. Как только что было показано, программист может использовать регистр A, чтобы выбрать либо область памяти данных для последующей Cинструкции, включающей M, либо область памяти инструкции для последующей C-инструкции, включающей прыжок. Таким образом, чтобы предотвратить конфликтующее использование регистра A, в хорошо написанных программах C-инструкция, которая может вызвать переход (то есть с некоторыми ненулевыми j битами), не должна содержать ссылку на M, и наоборот. 4.2.4 Символы Команды сборки могут ссылаться на области памяти (адреса), используя либо константы, либо символы. Символы вводятся в программы сборки следующими тремя способами: ■ Предварительно определенные символы: любая подгруппа адресов ОЗУ может быть использована любой программой сборки с использованием следующих предварительно определенных символов: ● Виртуальные регистры: для упрощения программирования сборки символы R0-R15 предварительно определены для обозначения адресов ОЗУ от 0 до 15 соответственно. ● Предопределенные указатели: символы SP, LCL, ARG, THIS и TH предопределены для обозначения адресов RAM от 0 до 4 соответственно. Обратите внимание, что каждая из этих областей памяти имеет две метки. Например, на адрес 2 можно ссылаться, используя R2 или ARG. Это синтаксическое соглашение будет играть роль при реализации виртуальной машины, обсуждаемой в главах 7 и 8. ● Указатели ввода / вывода: символы SCREEN и KBD предопределены для обозначения адресов ОЗУ 16384 (0x4000) и 24576 (0x6000) соответственно, которые являются базовыми адресами карт памяти экрана и клавиатуры. Использование этих устройств ввода / вывода будет объяснено позже. ■ Символы метки: эти определяемые пользователем символы, которые служат для обозначения пунктов назначения команд goto, объявляются псевдокомандой «(Xxx)». Эта директива определяет символ Xxx для обозначения места в памяти команд, содержащего следующую команду в программе. Метка может быть определена только один раз и может использоваться в любом месте программы сборки, даже до строки, в которой она определена. ■ Переменные символы: Любой определяемый пользователем символ Xxx, появляющийся в программе сборки, который не предопределен и не определен где-либо еще с помощью команды «(Xxx)», обрабатывается как переменная, и ассемблеру присваивается уникальный адрес памяти, начиная с по адресу ОЗУ 16 (0x0010). 4.2.5 Обработка ввода / вывода Платформа Hack может быть подключена к двум периферийным устройствам: экрану и клавиатуре. Оба устройства взаимодействуют с компьютерной платформой через карты памяти. Это означает, что рисование пикселей на экране достигается путем записи двоичных значений в сегмент памяти, связанный с экраном. Аналогично, прослушивание клавиатуры осуществляется путем чтения области памяти, связанной с клавиатурой. Физические устройства ввода-вывода и их карты памяти синхронизируются с помощью непрерывных циклов обновления. Экран. Компьютер Hack имеет черно-белый экран, организованный в виде 256 строк по 512 пикселей на строку. Содержимое экрана представлено картой памяти 8 КБ, которая начинается с адреса ОЗУ 16384 (0x4000). Каждая строка на физическом экране, начиная с верхнего левого угла экрана, представлена в ОЗУ 32 последовательными 16-разрядными словами. Таким образом, пиксель в строке r сверху и в столбце c слева отображается на бит c% 16 (считая от LSB до MSB) слова, расположенного в RAM [16384 + r · 32 + c / 16]. Чтобы записать или прочитать пиксель физического экрана, считывают или записывают соответствующий бит в карте памяти, находящейся в ОЗУ (1 = черный, 0 = белый). Пример: Клавиатура. Компьютер Hack взаимодействует с физической клавиатурой посредством карты памяти, состоящей из одного слова, расположенной в адресе ОЗУ 24576 (0x6000). При каждом нажатии клавиши на физической клавиатуре ее 16-битный код ASCII появляется в ОЗУ [24576]. Когда ни одна клавиша не нажата, в этом месте появляется код 0. В дополнение к обычным кодам ASCII клавиатура Hack распознает клавиши, показанные на рисунке 4.6. 4.2.6 Синтаксические соглашения и формат файла Файлы двоичного кода. Файл двоичного кода состоит из текстовых строк. Каждая строка представляет собой последовательность из шестнадцати символов «0» и «1» ASCII, кодирующих одну инструкцию машинного языка. Взятые вместе, все строки в файле представляют программу на машинном языке. Контракт таков, что когда программа машинного языка загружается в память команд компьютера, двоичный код, представленный n-й строкой файла, сохраняется в адресе n памяти команд (количество строк программы и адресов памяти начинается с 0). , По соглашению программы машинного языка хранятся в текстовых файлах с расширением «хак», например, Prog. хак. Файлы ассемблера. По соглашению программы на ассемблере хранятся в текстовых файлах с расширением «asm», например, Prog.asm. Файл на ассемблере состоит из текстовых строк, каждая из которых представляет либо инструкцию, либо объявление символа: Рисунок 4.6 Специальные коды клавиатуры на платформе Hack. ■ Инструкция: A-инструкция или C-инструкция. ■ (Символ): эта псевдокоманда заставляет ассемблер присваивать метку Символ месту памяти, в котором будет храниться следующая команда программы. Это называется «псевдокоманда», поскольку она не генерирует машинный код. (Остальные соглашения в этом разделе относятся только к программам сборки.) Константы и символы. Константы должны быть неотрицательными и всегда записываться в десятичной записи. Пользовательским символом может быть любая последовательность букв, цифр, подчеркивания (_), точки (.), Знака доллара ($) и двоеточия (:), которая не начинается с цифры. Комментарии. Текст, начинающийся с двух косых черт (//) и заканчивающийся в конце строки, считается комментарием и игнорируется. Пустое пространство. Символы пробела игнорируются. Пустые строки игнорируются. Условные обозначения. Вся мнемоника сборки должна быть написана заглавными буквами. Остальное (пользовательские метки и имена переменных) чувствительно к регистру. Соглашение состоит в том, чтобы использовать прописные буквы для меток и строчные буквы для имен переменных