Глава 17 Автоматизация

We use cookies. Read the Privacy and Cookie Policy

Человек часто демонстрирует удивительную находчивость и усердие, но в то же время ему присуща страшная лень. Всем известно, что люди не любят работать. Наше отвращение к работе так сильно, а изобретательность настолько выражена, что мы готовы потратить бесчисленные часы на проектирование и сборку устройств, которые могли бы урезать наш рабочий день на несколько минут. И мало что может доставить человеку больше удовольствия, чем видение, в котором он расслабленно качается в гамаке, наблюдая, как его новенькое изобретение косит газон. Боюсь, на следующих страницах вы не найдете чертежей автоматической газонокосилки. Однако в этой главе будет описан ряд устройств в порядке возрастания сложности, позволяющих автоматизировать процесс сложения и вычитания чисел. Понимаю, что подобное заявление вряд ли поразит вас. К концу этой главы мы соберем машину, которая сможет решить практически любую задачу, предполагающую выполнение сложения и вычитания, а это действительно весьма обширный класс.

Разумеется, чем устройство заковыристее, тем оно сложнее, поэтому часть информации может показаться трудной. Никто не будет винить вас, если пропустите излишне сложные фрагменты. Время от времени у вас будет возникать желание взбунтоваться и навсегда отказаться от попыток решить математическую задачу с помощью электрических и механических устройств. Обязательно следите за моей мыслью, потому что к концу главы мы изобретем машину, которую с полным правом можно назвать компьютером.

Последний рассмотренный нами сумматор был описан в главе 14. Эта версия включала 8-битную защелку, где накапливалась общая сумма чисел, введенных с помощью набора из восьми переключателей.

Как вы помните, 8-битная защелка использует триггеры для хранения 8-битного значения. Чтобы использовать это устройство, на мгновение нажмите кнопку «Очистка» для обнуления содержимого защелки. Затем с помощью переключателей введите первое число. Сумматор просто прибавит его к нулевому значению на выходе защелки, поэтому результатом будет введенное число. При нажатии кнопки «Сложить» это число сохранится в защелке и отобразится лампочками. Теперь с помощью переключателей введите второе число. Сумматор прибавит его к уже сохраненному в защелке.

Нажатие кнопки «Сложить» снова приводит к сохранению общей суммы в защелке и ее отображению с помощью лампочек. Так вы можете сложить множество чисел, отображая общую сумму, которая, правда, ограничена числом 255 при использовании восьми лампочек.

Когда я продемонстрировал эту схему в главе 14, вам было известно только о защелках со срабатыванием по уровню. Для сохранения данных в такой защелке нужно, чтобы значение входного сигнала Clk стало равным 1, а затем 0. Пока вход Clk — 1, сигналы на входах защелки для данных могут меняться, и эти изменения будут влиять на ее содержимое. Чуть позже я познакомил вас с защелками со срабатыванием по фронту, которые сохраняют данные в краткий промежуток времени, пока входной сигнал Clk изменяется с 0 на 1. Защелки со срабатыванием по фронту немного проще, поэтому в этой главе при описании защелок буду предполагать, что речь идет о защелках именно этого типа.

Защелка, применяемая для хранения текущего значения суммы, называется аккумулятором или накопителем. Однако далее мы увидим, что аккумулятору нет необходимости просто накапливать сумму. Часто это защелка, где сначала сохраняется одно число, а затем результат сложения или вычитания из него другого числа.

У показанной выше счетной машины существует серьезный недостаток, который сразу бросается в глаза. Допустим, существует список из 100 двоичных чисел, которые вы хотите сложить. Вы садитесь перед сумматором, старательно вводите каждое число и накапливаете сумму. По окончании процедуры обнаруживаете, что несколько чисел в списке были неправильными. В этом случае вам придется начинать все сначала.

А может, и нет. В предыдущей главе мы использовали около пяти миллионов реле для создания массива RAM емкостью 64 килобайта. Кроме того, мы собрали пульт управления, который позволяет замкнуть переключатель «Перехват» и фактически перехватить управление массивом RAM, чтобы производить запись и чтение данных с помощью переключателей.

Если бы вы ввели все 100 двоичных чисел в этот массив RAM, а не напрямую в сумматор, то сделать несколько исправлений было бы проще.

Итак, теперь перед нами стоит задача подключить массив RAM к сумматору, аккумулирующему итоговое значение. Очевидно, что вместо сигналов от переключателей на вход сумматора можно подать выходные сигналы массива RAM (DO), однако легко упустить тот факт, что 16-битный счетчик (вроде собранного в главе 14) способен управлять адресными сигналами. Сигналы массива RAM «Ввод данных» (DI) и «Запись» в этой схеме отсутствуют за ненадобностью.

Разумеется, это не самое простое счетное устройство. Перед его использованием нужно замкнуть переключатель «Очистка». При этом содержимое защелки обнуляется, а для выходного сигнала 16-битного счетчика задается значение 0000h. Затем вы замыкаете переключатель «Перехват» на пульте управления массивом RAM, после чего можете записать в память набор подлежащих сложению 8-битных чисел, начиная с адреса 0000h. Если собираетесь суммировать 100 чисел, сохраните их в ячейках памяти с адресами 0000h–0063h. Кроме того, вы должны записать значение 00h во все неиспользуемые ячейки памяти. Затем можете разомкнуть переключатель «Перехват» на пульте управления массивом RAM, чтобы снова передать сумматору управление памятью, а также разомкнуть переключатель «Очистка». Теперь можно расслабиться и любоваться мигающими лампочками.

Вот что происходит при совершении вышеописанных действий: при первом размыкании переключателя «Очистка» текущим адресом массива RAM является 0000h. Восьмибитное значение, сохраненное в ячейке по этому адресу, подается на один из входов сумматора. На другой вход подается значение 00h, поскольку содержимое защелки также обнулено.

Осциллятор генерирует тактовый сигнал, или синхросигнал, который быстро колеблется между значениями 0 и 1. После размыкания переключателя «Очистка» при каждом изменении синхросигнала с 0 на 1 одновременно происходят две вещи: в защелке сохраняется сумма из сумматора, а значение 16-битного счетчика увеличивается на единицу, то есть происходит обращение к следующему адресу в массиве RAM. При первом изменении значения синхросигнала с 0 на 1 после размыкания переключателя «Очистка» в защелке сохраняется первое число, а значение счетчика изменяется на 0001h. Во второй раз в защелке сохраняется сумма первого и второго чисел, а значение счетчика изменяется на 0002h. И так далее.

Разумеется, здесь я делаю некоторые допущения. Прежде всего предполагается, что частота колебаний осциллятора достаточно низкая, чтобы остальные компоненты схемы успевали сработать. При каждом изменении значения синхросигнала одни многочисленные реле должны активировать другие, чтобы на выходе сумматора появилось значение суммы.

Одна из проблем этой схемы в том, что мы не можем остановить ее работу. В какой-то момент лампочки перестанут мигать потому, что в оставшихся ячейках памяти хранится значение 00h. Тогда вы сможете считать двоичную сумму. Когда счетчик достигнет значения FFFFh, он обнулится (подобно одометру автомобиля), и автоматический сумматор снова начнет прибавлять числа к полученной ранее сумме.

У этого сумматора есть и другие недостатки. Он способен производить только сложение 8-битных чисел. Мало того, что в массиве RAM нельзя сохранить число больше 255, — сама сумма также ограничена значением 255. Кроме того, наш сумматор не способен производить вычитание, хотя вы можете выразить отрицательные числа с помощью дополнения до двух, в случае чего сумматор будет обрабатывать числа только в диапазоне от –128 до 127. Очевидно, что для суммирования больших значений (например, 16-битных) требуется удвоить ширину массива RAM, сумматора и защелки, а также добавить еще восемь лампочек. Однако к таким инвестициям вы можете оказаться не готовы.

Конечно, я бы даже не упомянул об этих проблемах, если бы не был уверен, что они решаемы. Осложнение, на котором я хотел бы сосредоточить ваше внимание в первую очередь, заключается в другом. Что, если вас не интересует одна сумма 100 чисел и вы хотите использовать сумматор для сложения 50 пар чисел и получения 50 разных сумм? Что, если вам нужна машина, способная складывать числа группами по два, десять и т. д., а также предоставлять удобный доступ к результатам своей работы?

Показанный ранее автоматизированный сумматор отображает текущую сумму с помощью набора лампочек, подключенных к защелке. Такая схема не подойдет, если вы решите сложить 50 пар чисел, чтобы получить 50 разных сумм. Вместо этого вы, вероятно, захотите, чтобы результаты сохранялись в массиве RAM. Тогда вы можете использовать пульт управления памятью для проверки результатов в удобное для вас время. Специально для этой цели такой пульт управления предусматривает дополнительный набор лампочек.

Получается, мы можем избавиться от лампочек, подключенных к защелке. Вместо этого выход защелки необходимо подключить ко входу DI массива RAM для записи сумм в память.

На этой схеме отсутствуют некоторые другие компоненты сумматора, в частности осциллятор и переключатель «Очистка». Я убрал их, поскольку уже не столь очевидно, откуда на входы счетчика и защелки поступают сигналы Clr и Clk. Более того, теперь, когда мы задействовали входы DI массива RAM, нам нужен способ управления его сигналом «Запись».

Давайте на мгновение оставим схему и сосредоточимся на стоящей перед нами задаче. Итак, мы хотим сконструировать сумматор, возможности которого не ограничиваются сохранением текущей суммы складываемых чисел, хотим полностью контролировать количество слагаемых, а также количество разных сумм, сохраняемых в памяти для последующего изучения.

Предположим, нам требуется сложить три числа, потом два, а затем еще три. Мы могли бы сохранить эти числа в массиве RAM, начиная с адреса 0000h, чтобы содержимое памяти выглядело следующим образом.

Таким образом я буду представлять раздел памяти. Прямоугольники — это ячейки памяти. Каждый байт находится в ячейке. Адрес ячейки указан слева. Нет необходимости указывать все адреса, поскольку они идут по порядку, и мы всегда можем выяснить, какой адрес соответствует той или иной ячейке. Приведенные справа комментарии указывают, что сумматор должен сохранить три суммы в пустых ячейках. (Несмотря на то что в этих прямоугольниках ничего нет, ячейки памяти необязательно пустые. В памяти всегда что-то содержится, даже если это просто случайные данные. Правда, в настоящий момент в ней нет ничего полезного.)

Знаю, вам не терпится попрактиковаться в сложении шестнадцатеричных чисел и самостоятельно заполнить пустые прямоугольники. Однако цель такой демонстрации не в этом. Нужно, чтобы автоматизированный сумматор сделал всю работу за нас.

Первая версия сумматора выполняла единственное действие — сложение содержимого ячейки памяти со значением в 8-битной защелке, которую я назвал аккумулятором. Теперь необходимо, чтобы сумматор выполнял четыре различных действия. Для начала суммирования потребуется, чтобы сумматор передал байт из памяти в аккумулятор.

Назовем эту операцию загрузкой. Вторая операция — сложение байта из памяти с содержимым аккумулятора, третья — сохранение в памяти суммы из аккумулятора. В конце нам нужно каким-то образом остановить работу сумматора.

Давайте подробно распишем действия, которые должен выполнить сумматор в данном конкретном примере:

загрузить значение из ячейки 0000h в аккумулятор;

сложить значение из ячейки 0001h со значением в аккумуляторе;

сложить значение из ячейки 0002h со значением в аккумуляторе;

сохранить содержимое аккумулятора в ячейке 0003h;

загрузить значение из ячейки 0004h в аккумулятор;

сложить значение из ячейки 0005h со значением в аккумуляторе;

сохранить содержимое аккумулятора в ячейке 0006h;

загрузить значение из ячейки 0007h в аккумулятор;

сложить значение из ячейки 0008h со значением в аккумуляторе;

сложить значение из ячейки 0009h со значением в аккумуляторе;

сохранить содержимое аккумулятора в ячейке 000Ah;

остановить работу сумматора.

Обратите внимание: как и в исходной версии сумматора, адресация байтов памяти происходит последовательно, начиная с ячейки 0000h. Исходный сумматор просто прибавлял содержимое ячейки памяти по этому адресу к содержимому аккумулятора. В некоторых случаях это действие по-прежнему уместно. Однако иногда нам требуется загрузить значение из памяти непосредственно в аккумулятор или сохранить содержимое аккумулятора в памяти. После выполнения всех этих операций мы хотим, чтобы сумматор прекратил работу и можно было бы проверить содержимое памяти.

Как это реализовать? Важно понимать, что мы не можем просто записать в память кучу чисел и ожидать, что сумматор догадается, что с ними делать. Для каждого числа в массиве RAM необходимо предусмотреть некий числовой код, соответствующий операциям «Загрузка», «Сложение», «Сохранение» и «Остановка».

Вероятно, проще всего (и, разумеется, затратнее) хранить эти коды в отдельном массиве RAM. Доступ к этому второму массиву осуществляется одновременно с доступом к исходному массиву. Однако вместо слагаемых он будет содержать коды, указывающие на то, что должен сделать сумматор с содержимым соответствующей ячейки исходного массива RAM. Исходный массив можно обозначить словом «Данные», а новый массив — словом «Код».

Мы уже выяснили, что наш новый сумматор должен записывать суммы в исходный массив «Данные». Однако в новый массив «Код» значения будут сохраняться только при помощи пульта управления.

Нам нужны четыре кода для обозначения четырех действий, которые должен выполнять новый сумматор. Мы можем назначить этим действиям любые коды, например следующие.

Операция

Код

Загрузить

10h

Сохранить

11h

Сложить

20h

Остановить

FFh

Итак, чтобы выполнить три операции сложения из приведенного выше примера, нужно использовать пульт управления для сохранения следующих значений в массиве «Код».

Возможно, вам захочется сравнить содержимое этого массива RAM с массивом, в котором хранятся слагаемые. В результате вы заметите, что каждый код в массиве «Код» соответствует значению в массиве «Данные», которое должно быть загружено в аккумулятор, прибавлено к его содержимому или сохранено в памяти. Используемые таким образом числовые коды часто называются кодами команд или кодами операций. Они дают схеме «команду» выполнить определенную «операцию».

Как я уже упоминал, выход 8-битной защелки исходного сумматора должен быть входом массива RAM «Данные». Так работает команда «Сохранить». Однако нам требуется внести еще одно изменение: изначально выход 8-битного сумматора — вход 8-битной защелки. Теперь для выполнения команды «Загрузить» выход массива «Данные» иногда должен соединяться со входом 8-битной защелки. Для этого необходим селектор двух линий на одну. Пересмотренная схема сумматора выглядит следующим образом.

На этой схеме еще чего-то не хватает, но она отображает все 8-битные потоки данных между различными компонентами. Шестнадцатибитный счетчик предоставляет адреса для двух массивов RAM. Выход массива «Данные» подключен ко входу 8-битного сумматора для выполнения команды «Сложить». Правда, ко входу 8-битной защелки может быть подключен либо выход массива «Данные» (в случае выполнения команды «Загрузить»), либо выход сумматора (в случае выполнения команды «Сложить»). В этой ситуации требуется селектор «2 на 1». Выходной сигнал защелки не только возвращается обратно в сумматор, но и подается на вход массива «Данные» для выполнения операции «Сохранить».

На схеме не хватает только контролирующих эти компоненты сигналов, которые называются управляющими; к ним относятся входы Clk и Clr 16-битного счетчика, входы Clk и Clr 8-битной защелки, вход W массива «Данные» и вход Sel селектора «2 на 1». Некоторые из этих сигналов, очевидно, будут основываться на выходе массива «Код». Например, вход Sel селектора «2 на 1» должен быть равен 0 (выбран выход «Данные» массива RAM), если выходной сигнал массива «Код» соответствует команде «Загрузить». Вход W массива «Данные» должен быть равен 1 только тогда, когда код соответствует команде «Сохранить». Эти управляющие сигналы могут генерироваться различными комбинациями логических вентилей.

Добавив несколько дополнительных компонентов и код новой команды, можем сделать так, чтобы наша схема вычитала число из значения, хранящегося в аккумуляторе. Первым делом нужно расширить таблицу кодов команд.

Операция

Код

Загрузить

10h

Сохранить

11h

Сложить

20h

Вычесть

21h

Остановить

FFh

Коды команд «Сложить» и «Вычесть» отличаются только младшим битом значения, который мы будем называть C0. Если значение кода команды равно 21h, то схема должна делать то же самое, что и в случае выполнения команды «Сложить», за исключением того, что данные из массива «Данные» инвертируются перед попаданием в сумматор, а для входа сумматора CI задается значение 1. Сигнал C0 может выполнять обе операции в обновленном сумматоре, дополненном инвертором.

Предположим, нам нужно сложить два числа: 56h и 2Ah, а затем из полученной суммы вычесть 38h. Это можно сделать, используя следующие коды и данные, хранящиеся в двух массивах RAM.

После выполнения операции «Загрузить» аккумулятор содержит значение 56h, после операции «Сложить» — сумму 56h и 2Ah, то есть 80h. Операция «Вычесть» приводит к инвертированию битов следующего значения в массиве «Данные» (38h). Инвертированное значение C7h прибавляется к 80h, при этом вход сумматора для переноса (CI) равен 1.

Результатом будет 48h (в десятичной системе счисления: 86 + 42 – 56 = 72).

Еще одной нерешенной проблемой остается недостаточная ширина канала данных сумматора и всех остальных подключенных к нему устройств. Ранее я предлагал удвоить количество 8-битных сумматоров (и всех остальных компонентов), чтобы получить 16-битные устройства.

Однако мы можем использовать гораздо менее дорогостоящее решение. Предположим, нужно сложить два 16-битных числа, например следующие.

Для получения суммы двух 16-битных чисел отдельно сложим их младшие байты (крайние справа).

А затем старшие (крайние слева).

В результате получится число 99D7h. Если мы сохраним два 16-битных числа в памяти, как показано на рисунке, результат D7h будет сохранен по адресу 0002h, а 99h — по адресу 0005h.

Разумеется, это будет работать не всегда. Такой метод подходит для сложения чисел, выбранных в качестве примера. Если нам требуется сложить числа 76ABh и 236Ch, при сложении двух младших байтов возникает перенос.

Этот перенос должен быть прибавлен к сумме двух старших байтов для получения окончательного результата — 9A17h.

Можем ли мы сделать так, чтобы наш сумматор корректно складывал два 16-битных числа? Да, для этого достаточно сохранить бит переноса, полученный от 8-битного сумматора при выполнении первой операции сложения, а затем подать этот бит на вход сумматора для переноса при следующем сложении. Как можно сохранить этот бит? С помощью однобитной защелки (на этот раз назовем ее защелкой для переноса).

Использование защелки для переноса требует еще одного кода команды — «Сложить с переносом». При сложении 8-битных чисел используется обычная команда «Сложить». На вход сумматора для переноса (CI) подается значение 0, а значение, возникающее на выходе для переноса (CO), сохраняется в защелке для переноса (хотя в его использовании вообще нет необходимости).

При суммировании двух 16-битных чисел используем обычную команду «Сложить» для сложения младших байтов. Вход сумматора CI равен 0, а значение выхода CO сохраняется в защелке для переноса. Для сложения двух старших байтов будем использовать новую команду «Сложить с переносом». В данном случае при сложении двух чисел на вход сумматора CI подается значение, сохраненное в защелке для переноса. Таким образом, если в результате первой операции сложения возник перенос, этот бит переноса используется при втором сложении. Если переноса не возникло, выходное значение защелки для переноса равно 0.

Для вычитания одного 16-битного числа из другого потребуется еще одна новая команда — «Вычесть с заимствованием». Как правило, выполнение команды «Вычесть» предполагает инвертирование вычитаемого и подачу на вход сумматора CI значения 1. В этом случае выход для переноса также равен 1, — это нормально, на это явление можно не обращать внимания. Однако при вычитании 16-битного числа значение этого выхода необходимо сохранить в защелке для переноса. При втором вычитании это значение должно быть подано на вход сумматора CI.

Учитывая две новые операции, «Сложить с переносом» и «Вычесть с заимствованием», в общей сложности мы имеем семь кодов команд.

Операция

Код

Загрузить

10h

Сохранить

11h

Сложить

20h

Вычесть

21h

Сложить с переносом

22h

Вычесть с заимствованием

23h

Остановить

FFh

Число, подаваемое в сумматор, инвертируется при выполнении операции «Вычесть» или «Вычесть с заимствованием». Выходной сигнал сумматора CO подается на вход защелки для переноса. Он сохраняется в защелке всякий раз, когда выполняются операции «Сложить», «Вычесть», «Сложить с переносом» или «Вычесть с заимствованием». Значение входа для переноса 8-битного сумматора устанавливается в 1 при выполнении операций «Вычесть», или «Сложить с переносом», или «Вычесть с заимствованием», когда выход защелки для переноса равен 1.

В результате выполнения команды «Сложить с переносом» значение входа сумматора для переноса бывает равно 1 только в том случае, когда при выполнении предыдущей команды «Сложить» или «Сложить с переносом» на выходе из сумматора возник перенос. Таким образом, мы используем команду «Сложить с переносом» всякий раз, когда складываем многобайтные числа, вне зависимости от того, есть ли в этой операции необходимость. Вот как следует закодировать продемонстрированную ранее операцию сложения 16-битных чисел.

Такая последовательность операций позволяет получить правильный ответ независимо от того, какие цифры мы складываем.

Добавление двух новых кодов команд значительно расширило функционал сумматора. Мы больше не ограничиваемся сложением 8-битных значений. Многократное использование команды «Сложить с переносом» позволяет складывать 16-, 24-, 32-, 40-битные значения и т. д. Предположим, нам нужно сложить 32-битные числа 7A892BCDh и 65A872FFh. Для этого потребуется лишь одна команда «Сложить» и три команды «Сложить с переносом».

Конечно, вводить эти числа в память не очень удобно. При этом не только приходится использовать переключатели для представления двоичных чисел. Сами числа сохраняются в несмежных ячейках, например 7A892BCDh оказывается в ячейках 0000h, 0003h, 0006h и 0009h, начиная с младшего байта. Для получения окончательного результата необходимо проверить значения, хранящиеся в ячейках 0002h, 0005h, 0008h и 000Bh.

Более того, текущая конструкция нашего сумматора не допускает повторного использования результатов в последующих операциях. Допустим, нужно сложить три 8-битных числа, а затем вычесть из этой суммы другое 8-битное число и сохранить результат. Для этого потребуются команда «Загрузить», две команды «Сложить», команды «Вычесть» и «Сохранить». А если нужно вычесть из этой исходной суммы другие числа, в то время как сумма эта недоступна, поскольку каждый раз нам пришлось бы ее пересчитывать? Проблема в том, что созданный нами сумматор обращается к ячейкам массивов «Код» и «Данные» одновременно и последовательно, начиная с адреса 0000h. Каждая команда в массиве «Код» соответствует ячейке массива «Данные» по тому же адресу. Когда в результате выполнения команды «Сохранить» в массиве «Данные» сохраняется некоторое значение, в дальнейшем оно уже не может быть загружено в аккумулятор.

Чтобы решить эту проблему, я намерен внести в сумматор фундаментальное и радикальное изменение, которое поначалу может показаться безумно сложным. Однако со временем (надеюсь) вы оцените ту гибкость, которую оно обеспечивает.

Итак, в настоящее время у нас есть семь кодов команд.

Операция

Код

Загрузить

10h

Сохранить

11h

Сложить

20h

Вычесть

21h

Сложить с переносом

22h

Вычесть с заимствованием

23h

Остановить

FFh

Каждый из этих кодов занимает в памяти один байт. Теперь нужно, чтобы эти коды, за исключением кода команды «Остановить», занимали по три байта. Первый байт будет занят самим кодом, а в следующих двух будет храниться 16-битный адрес ячейки памяти. Для команды «Загрузить» этот адрес указывает местоположение в массиве «Данные», где содержится байт для загрузки в аккумулятор. Для команд «Сложить», «Вычесть», «Сложить с переносом» и «Вычесть с заимствованием» этот адрес указывает местоположение байта, который должен быть прибавлен или вычтен из значения, содержащегося в аккумуляторе. Для команды «Сохранить» этот адрес указывает местоположение, где нужно сохранить содержимое аккумулятора. Например, простейшая задача для текущей версии сумматора — сложение двух чисел. Для этого в массивы «Код» и «Данные» необходимо внести следующие значения.

В пересмотренной версии сумматора для хранения каждой команды, кроме «Остановить», требуется три байта.

За каждым из кодов команд, кроме «Остановить», следуют два байта, указывающие 16-битный адрес в массиве «Данные». В данном примере этими тремя адресами являются 0000h, 0001h и 0002h, однако они могут быть какими угодно.

Ранее я продемонстрировал процесс сложения двух 16-битных чисел, в частности 76ABh и 232Ch, с использованием команд «Сложить» и «Сложить с переносом». Однако нам пришлось сохранить два младших байта этих чисел в ячейках 0000h и 0001h и два старших байта — в ячейках 0003h и 0004h. Результат сложения был сохранен в ячейках 0002h и 0005h.

Благодаря этому изменению мы можем хранить слагаемые и результат более разумным способом, возможно, в той области памяти, которую еще никогда не использовали.

Эти шесть ячеек не обязательно должны располагаться последовательно. Они могут быть разбросаны по всему массиву «Данные». Для сложения чисел, находящихся в этих ячейках памяти, в массиве «Код» необходимо сохранить следующие значения.

Обратите внимание: сначала складываются два младших байта, хранящихся в ячейках 4001h и 4003h, а результат сохраняется в ячейке 4005h. Два старших байта (содержащихся в ячейках 4000h и 4002h) складываются с использованием команды «Сложить с переносом», а результат сохраняется в ячейке 4004h. Если удалим команду «Остановить» и добавим новые команды в массив «Код», то при выполнении последующих расчетов сможем использовать исходные слагаемые и их сумму путем простого обращения к соответствующим адресам.

Ключевым моментом при реализации этой идеи является подключение выхода массива «Код» DO к трем 8-битным защелкам. В каждой из этих защелок хранится один из байтов 3-байтной команды. Первая защелка содержит код команды, вторая — старший байт адреса, третья — младший байт адреса. Выход второй и третьей защелок становится 16-битным адресом ячейки в массиве «Данные».

Процесс извлечения команды из памяти называется выборкой команды. Каждая команда в нашем сумматоре занимает три байта, и она извлекается из памяти по одному байту за раз; для извлечения команды требуются три цикла синхросигнала, для всего командного цикла — четыре. Эти изменения, безусловно, усложняют систему управляющих сигналов.

Когда сумматор производит серию действий в соответствии с кодом, это называется выполнением команды. Однако это не значит, что машина живая. Она не анализирует машинный код и не решает, что ей делать. Каждый машинный код просто особым образом запускает различные управляющие сигналы, заставляя машину выполнять различные действия.

Важно: расширение функционала сумматора приводит к замедлению его работы. При той же частоте осциллятора машина складывает числа в четыре раза медленнее по сравнению с первым сумматором, описанным в этой главе. Это проявление инженерного принципа «Бесплатных завтраков не бывает»[20], смысл которого в том, что улучшение одного аспекта машины приводит к ухудшению другого.

Если бы вы на самом деле собирали такой сумматор из реле, то основными компонентами схемы, очевидно, были бы два массива RAM емкостью 64 килобайта. В самом начале вы, вероятно, сэкономили на этих компонентах, решив, что пока можете обойтись одним килобайтом памяти. Если бы вы были уверены, что сохраните все данные в ячейках с 0000h по 03FFh, то могли бы спокойно использовать память емкостью менее 64 килобайта.

Впрочем, вы, вероятно, не в восторге от необходимости использовать два массива RAM. На самом деле это необязательно. Я ввел два массива RAM (один с кодами и один с данными) для того, чтобы архитектура сумматора была максимально понятной и простой. Но сейчас, когда мы решили, что каждая команда будет занимать три байта (причем второй и третий байты будут содержать адрес, где находятся данные), нам не нужны два отдельных массива RAM. Коды команд и данные можно хранить в одном массиве.

Чтобы реализовать это, нужен селектор «2 на 1» для определения способа адресации массива RAM. Как правило, один адрес, как и раньше, подается на вход селектора с 16-битного счетчика. Выход DO массива RAM по-прежнему подключен к трем защелкам, в которых сохраняются код команды и два байта адреса, сопровождающего каждую команду. Однако 16-битный адрес подается и на второй вход селектора «2 на 1». После сохранения этого адреса в защелках селектор передает его на адресный вход массива RAM.

Мы значительно продвинулись. Теперь можно ввести команды и данные в один массив RAM. Например, на следующей диаграмме показан процесс сложения двух 8-битных чисел и вычитания из полученной суммы третьего числа.

Как обычно, команды сохраняются начиная с адреса 0000h, поскольку именно с этой ячейки счетчик начинает адресацию массива RAM после обнуления. Последняя команда «Сохранить» хранится по адресу 000Ch. Мы могли бы сохранить три числа и результаты в любом месте массива RAM (разумеется, за исключением первых 13 байт, поскольку они заняты кодами команд), однако решили остановиться на данных, начиная с ячейки по адресу 0010h.

Предположим, что нам понадобилось прибавить к результату еще два числа. Можно, конечно, заменить все только что введенные команды новыми, но мы не хотим этого делать. Возможно, мы предпочли бы после выполнения этих команд просто выполнить новые, заменив перед этим код команды «Остановить» кодом новой команды «Загрузить» в ячейке 000Ch. Однако нам требуются две новые команды «Сложить», команда «Сохранить» и новая команда «Остановить». Единственная проблема: в ячейке 0010h хранятся данные. Их необходимо переместить дальше, изменив при этом ссылающиеся на них команды.

Может показаться, что объединение кода и данных в одном массиве RAM не было такой уж хорошей идеей. Уверяю вас, рано или поздно такая проблема обязательно бы возникла. Так что давайте решим ее. В данном случае можно попробовать ввести коды новых команд, начиная с адреса 0020h, а новые данные — с адреса 0030h.

Обратите внимание: первая команда «Загрузить» ссылается на ячейку 0013h, в которой хранится результат первого расчета.

Итак, с адреса 0000h в памяти хранятся команды, с 0010h — некоторые данные, с 0020h — еще команды, а с адреса 0030h — еще данные. Нам нужно, чтобы сумматор выполнил все команды, начиная с адреса 0000h.

Мы знаем, что следует удалить команду «Остановить» из ячейки 000Ch. Под словом «удалить» я подразумеваю ее замену чем-то другим. Достаточно ли этого?

Проблема в том, что все, чем мы заменим команду «Остановить», будет интерпретироваться как код команды. Это касается и того, что будет храниться через каждые три ячейки после него — по адресам 000Fh, 0012h, 0015h, 0018h, 001Bh и 001Eh. Что, если одним из этих значений окажется число 11h, которое соответствует команде «Сохранить»? Что, если два байта после кода команды «Сохранить» будут ссылаться на ячейку 0023h? Это заставит сумматор сохранить содержимое аккумулятора в этой ячейке. Однако в ней уже содержится что-то важное! И даже если ничего подобного не произойдет, после кода команды по адресу 001Eh сумматор извлечет код из ячейки 0021h, а не 0020h, где на самом деле находится код нашей следующей команды.

Все ли согласны, что мы не можем просто удалить команду «Остановить» из ячейки 000Ch и надеяться на лучшее?

Мы можем заменить ее новой командой под названием «Перейти». Давайте добавим ее в наш репертуар.

Операция

Код

Загрузить

10h

Сохранить

11h

Сложить

20h

Вычесть

21h

Сложить с переносом

22h

Вычесть с заимствованием

23h

Перейти

30h

Остановить

FFh

Обычно массив RAM в сумматоре адресуется последовательно. Команда «Перейти» заставляет машину действовать иначе, то есть обращаться к ячейке массива по другому заданному адресу. Такая команда иногда называется командой ветвления.

В предыдущем примере мы можем заменить команду «Остановить» в ячейке 000Ch командой «Перейти».

Значение 30h соответствует коду команды «Перейти». Шестнадцатибитный адрес, который следует за ним, указывает на ячейку со следующей командой, к которой должен обратиться сумматор.

В предыдущем примере сумматор, как обычно, начинает с ячейки 0000h, обрабатывает команды «Загрузить», «Сложить», «Вычесть» и «Сохранить». Затем он выполняет команду «Перейти» и продолжает работу с адреса 0020h, начиная с которого хранятся команда «Загрузить», две команды «Сложить», команды «Сохранить» и «Остановить».

Команда «Перейти» влияет на 16-битный счетчик. Всякий раз, когда сумматор ее запускает, на выходе счетчика должен возникать новый адрес, следующий за кодом команды «Перейти». Это реализуется с помощью входов Pre и Clr триггеров D-типа со срабатыванием по фронту, из которых состоит 16-битный счетчик.

Напомню, что входы Pre и Clr должны быть равны 0 при выполнении обычной операции. Если вход Pre равен 1, сигнал Q тоже становится 1; если вход Clr равен 1, сигнал Q — 0.

Если хотите загрузить в триггер новое значение (назовем его A — от слова «адрес»), вы можете включить его в схему следующим образом.

Обычно сигнал «Задать» равен 0. В этом случае вход триггера Pre также 0. Вход Clr равен 0 при условии, что сигнал «Сброс» не равен 1. Это позволяет очистить триггер независимо от значения сигнала «Задать». Когда сигнал «Задать» — 1, вход Pre — 1, а вход Clr — 0 при условии, что сигнал A — 1. Если сигнал A — 0, то вход Pre — 0, а вход Clr — 1. Следовательно, значение выхода Q будет совпадать со значением A.

Нам требуется по одной такой схеме для каждого бита 16-битного счетчика. После загрузки конкретного значения счетчик будет продолжать подсчет с него. В других отношениях изменения не являются такими серьезными. Шестнадцатибитный адрес из массива RAM, сохраненный в защелках, подается как на вход селектора «2 на 1» (передает его на адресный вход массива RAM), так и на вход 16-битного счетчика для выполнения функции «Задать».

Очевидно, сигнал «Задать» должен быть равен 1, только если код команды 30h и адрес сохранен в защелках.

Команда «Перейти», безусловно, полезна. Однако еще более практичной была бы команда, которая совершает переход не всегда, а только при определенных условиях. Такая команда называется условным переходом. Вероятно, лучший способ продемонстрировать ее полезность — постановка вопроса: «Как сделать так, чтобы наш сумматор перемножил два 8-битных числа, например A7h и 1Ch?»

Все просто, не так ли? Результат умножения двух 8-битных значений — 16-битное число. Для удобства все три числа, участвующие в этой операции, выражены в виде 16-битных значений. Первым делом нужно решить, где следует сохранить эти числа и результат.

Все знают, что умножить число A7h на 1Ch (соответствует десятичному числу 28) — это то же самое, что найти сумму 28 чисел A7h. Таким образом, в ячейках 1004h и 1005h фактически будет накапливаться 16-битный результат этого суммирования. Вот последовательность кодов для выполнения первой операции сложения.

После выполнения этих шести команд 16-битное значение в ячейках 1004h и 1005h будет равно числу A7h, умноженному на 1. Чтобы это значение равнялось произведению A7h и 1Ch, эти шесть команд необходимо выполнить еще 27 раз. Вы можете ввести эти шесть команд еще 27 раз, начиная с адреса 0012h, или сохранить код команды «Остановить» по адресу 0012h, нажать кнопку «Сброс» 28 раз, чтобы получить окончательный ответ.

Конечно, ни один из этих двух вариантов не идеален. Оба они предполагают, что вы должны что-то сделать: ввести набор команд или нажать кнопку «Сброс» в соответствии со значением одного из сомножителей. Уверен, что вы не хотели бы всю жизнь перемножать 16-битные значения именно так.

А что, если поместить команду «Перейти» в ячейку 0012h? Эта команда заставляет счетчик снова начать счет с ячейки 0000h.

Вроде бы проблема решена. При выполнении первой команды 16-битное значение в ячейках 1004h и 1005h будет равно произведению чисел A7h и 1. Затем благодаря команде «Перейти» мы вернемся к началу. После выполнения второй операции 16-битный результат будет равен произведению чисел A7h и 2. В конце концов он станет равен произведению чисел A7h и 1Ch, однако у нас нет способа остановить работу сумматора. Нам нужна команда «Перейти», которая запускала бы процесс с начала столько раз, сколько необходимо. Это условный переход, который совсем несложно реализовать. Сначала нужно добавить однобитную защелку, подобную защелке для переноса. Она будет называться нулевой защелкой, поскольку в ней будет сохраняться значение 1, только если все выходы 8-битного сумматора будут равны 0.

Выход этого 8-битного вентиля ИЛИ-НЕ равен 1, только если все входы равны 0. Как и вход защелки для переноса Clk, вход Clk нулевой защелки сохраняет значение только при выполнении команд «Сложить», «Вычесть», «Сложить с переносом» или «Вычесть с заимствованием». Это сохраненное значение называется флагом нуля. Будьте внимательны: флаг нуля равен 1, если все выходы сумматора равны 0; флаг нуля — 0, если не все выходы сумматора равны 0.

Защелка для переноса и нулевая защелка позволяют дополнить наш репертуар четырьмя командами.

Операция

Код

Загрузить

10h

Сохранить

11h

Сложить

20h

Вычесть

21h

Сложить с переносом

22h

Вычесть с заимствованием

23h

Перейти

30h

Перейти, если 0

31h

Перейти, если перенос

32h

Перейти, если не 0

33h

Перейти, если не перенос

34h

Остановить

FFh

Например, при выполнении команды «Перейти, если не 0» переход к указанному адресу осуществляется только в том случае, если выход нулевой защелки равен 0. Другими словами, никакого перехода не произойдет, если результат выполнения последней команды — «Сложить», «Вычесть», «Сложить с переносом» или «Вычесть с заимствованием» — равен 0. Реализация этого нововведения предполагает дополнение набора управляющих сигналов, которые реализуют обычную команду «Перейти»: при выполнении команды «Перейти, если не 0» сигнал 16-битного счетчика «Задать» равен 1 только при нулевом значении флага нуля.

Следующая последовательность команд, начинающаяся с адреса 0012h, позволяет перемножить два числа.

При первом выполнении этой последовательности команд в ячейках 0004h и 0005h, как мы уже выяснили, хранится 16-битное произведение чисел A7h и 1. Приведенные здесь команды загружают байт из ячейки 1003h в аккумулятор. Его значение — 1Ch. Этот байт прибавляется к значению в ячейке 001Eh, в которой хранится команда «Остановить», представленная действительным числом. Сложение чисел FFh и 1Ch равнозначно вычитанию 1 из числа 1Ch, поэтому результат равен 1Bh. Это значение не равно 0, поэтому флаг нуля — 0. Байт 1Bh сохраняется в ячейке 1003h. Затем выполняется команда «Перейти, если не 0». Флаг нуля не равен 1, поэтому переход происходит. На следующем этапе запускается команда, хранящаяся в ячейке 0000h. Имейте в виду, что команда «Сохранить» не влияет на флаг нуля. На его значение воздействуют только команды «Сложить», «Вычесть», «Сложить с переносом» или «Вычесть с заимствованием», поэтому его значение останется таким, какое было установлено в последний раз, когда отрабатывалась одна из этих команд.

При втором выполнении последовательности команд в ячейках 1004h и 1005h будет содержаться 16-битное произведение чисел A7h и 2. При сложении 1Bh и FFh получается 1Ah. Это значение не равно 0, поэтому мы возвращаемся к началу.

При двадцать восьмом выполнении этой последовательности команд в ячейках 1004h и 1005h будет содержаться произведение чисел A7h и 1Ch. В ячейке 1003h будет храниться значение 1, которое прибавится к FFh, в результате чего получится 0. При этом значение флага нуля станет 1, поэтому вместо команды «Перейти, если 0» будет выполнена команда «Остановить».

Теперь я могу объявить, что нам наконец удалось собрать устройство, которое мы можем на полном основании назвать компьютером. Несмотря на примитивность, это уже компьютер. Его отличие заключается в условном переходе. Контролируемое повторение последовательности команд, или цикл, — это то, что отличает компьютеры от калькуляторов. Я только что продемонстрировал, как команда условного перехода позволяет машине перемножить два числа. Подобным образом она может выполнить и деление. Кроме того, она не ограничивается 8-битными значениями и способна складывать, вычитать, умножать и делить 16-, 24-, 32-битные числа и числа большей разрядности. Это значит, что такая машина может вычислять квадратные корни, логарифмы и тригонометрические функции.

Когда мы собрали компьютер, можем начать использовать соответствующую терминологию.

Собранный компьютер можно классифицировать как цифровой, поскольку он работает с дискретными числами. К другому типу относятся аналоговые компьютеры, которые в настоящее время уже практически не применяются. (Цифровые, или дискретные, данные могут принимать только определенные значения[21]. Аналоговые данные непрерывны и могут принимать любое значение в заданном диапазоне.)

Цифровой компьютер состоит из четырех основных частей: процессорапамяти, как минимум одного устройства ввода и одного устройства вывода. В нашей машине память — массив RAM емкостью 64 килобайт. Устройства ввода и вывода — ряд переключателей и лампочек на пульте управления массивом RAM. Эти переключатели и лампочки позволяют нам (людям) вводить в память числа и анализировать результаты.

Все остальное — процессор, который также называется центральным процессорным устройством (ЦПУ). В обычной речи процессор иногда называют мозгом компьютера, но я не хотел бы использовать этот термин, поскольку собранное нами устройство совершенно не похоже на мозг. В наши дни часто употребляется слово «микропроцессор» — процессор, который благодаря использованию технологии, которую я опишу в главе 18, отличается очень малыми размерами. К тому, что мы собрали из реле в этой главе, вряд ли применима приставка «микро-».

Созданный нами процессор является 8-битным. Ширина аккумулятора и большинства каналов данных составляет 8 бит. Шестнадцатибитное значение подается только на адресный вход массива RAM. Если бы значение адреса было 8-битным, то вместо 65 536 байт можно было бы адресовать только 256 байт памяти, что было бы значительным ограничением.

Процессор состоит из нескольких компонентов. Я уже упомянул аккумулятор, который представляет собой просто защелку, позволяющую хранить число внутри процессора. Восьмибитный инвертор и 8-битный сумматор нашего компьютера вместе образуют арифметико-логическое устройство (АЛУ). Наше АЛУ выполняет только арифметические операции, в частности сложение и вычитание. В более сложных компьютерах АЛУ может производить такие логические функции, как И, ИЛИ и исключающее ИЛИ. Шестнадцатибитный счетчик называется счетчиком команд.

Собранный нами компьютер состоит из реле, проводов, переключателей и лампочек. Все это — аппаратное обеспечение (hardware, или хард) компьютера. Напротив, содержащиеся в памяти коды команд и числа называются программным обеспечением (software, или софт).

Когда мы говорим о компьютерах, термин «программное обеспечение» практически идентичен термину «компьютерная программа» (или просто «программа»). Написание программного обеспечения называется программированием. Именно этим я занимался, когда определял серию команд, позволяющих нашему компьютеру перемножить два числа.

Как правило, в компьютерных программах мы можем различать код (который относится к самим командам) и данные, то есть числа, которыми манипулирует код. Иногда это различие не так очевидно. Например, команда «Остановить» в описанной выше последовательности выступала еще и в качестве числа –1.

Компьютерное программирование иногда называют написанием кода, или кодированием. От программиста вы можете услышать фразы типа: «Я потратил свои каникулы на кодирование», «Я писал код до семи утра». Иногда компьютерных программистов называют кодерами, хотя кому-то этот термин может показаться уничижительным. Такие программисты, возможно, предпочтут, чтобы их называли программными инженерами.

Коды команд, на которые реагирует процессор (например, 10h и 11h, соответствующие командам «Загрузить» и «Сохранить»), называются машинными кодами, или машинным языком. В данном случае термин «язык» используется потому, что он напоминает устный или письменный человеческий язык в том смысле, что машина его «понимает» и реагирует на него.

Для обозначения команд, выполняемых нашей машиной, я использовал довольно длинные фразы вроде «Сложить с переносом». Как правило, команды — это короткие мнемонические коды, написанные заглавными буквами. Мнемокоды могут состоять всего из двух или трех букв. Далее перечислены возможные мнемокоды для команд, распознаваемых нашим компьютером.

Операция

Код

Мнемокод

Загрузить

10h

LOD

Сохранить

11h

STO

Сложить

20h

ADD

Вычесть

21h

SUB

Сложить с переносом

22h

ADC

Вычесть с заимствованием

23h

SBB

Перейти

30h

JMP

Перейти, если 0

31h

JZ

Перейти, если перенос

32h

JC

Перейти, если не 0

33h

JNZ

Перейти, если не перенос

34h

JNC

Остановить

FFh

HLT

Эти мнемокоды особенно полезны, когда используются в сочетании с другими сокращенными обозначениями. Например, вместо длинной команды «Загрузить байт из ячейки 1003h в аккумулятор» можем написать следующее.

LOD A,[1003h]

Обозначения A и [1003h] справа от мнемокода называются аргументами, которые определяют, что конкретно происходит при выполнении команды «Загрузить». При записи аргументов место назначения указывается слева (А — аккумулятор), а источник — справа. Квадратные скобки означают, что в аккумулятор нужно загрузить не число 1003h, а значение, хранящееся в ячейке памяти по адресу 1003h.

Аналогично команду «Сложить байт из ячейки 001Eh со значением в аккумуляторе» можно сократить до такого выражения.

ADD A, [001Eh]

А команду «Сохранить содержимое аккумулятора по адресу 1003h» — до выражения следующего вида.

STO [1003h], A

Обратите внимание: место назначения (ячейка памяти для команды «Сохранить») по-прежнему указывается слева, а источник — справа. Содержимое аккумулятора нужно сохранить в ячейке 1003h. Длинную команду «Перейти к ячейке 0000h, если флаг нуля не равен 1» можно записать кратко.

JNZ 0000h

В этой команде квадратные скобки не используются, поскольку она предполагает переход к ячейке 0000h, а не к значению, которое может в ней храниться.

Такая сокращенная запись удобна, поскольку позволяет перечислить команды в столбик и сделать их легко читаемыми, не прибегая к рисованию ячеек памяти. Для указания ячейки, в которой хранится определенная команда, можете использовать шестнадцатеричный адрес с двоеточием.

0000h: LOD A, [1005h]

А вот так можно указать на данные, хранящиеся по определенному адресу.

1000h: 00h, A7h

1002h: 00h, 1Ch

1004h: 00h, 00h

Два байта, разделенные запятой, указывают на то, что первый хранится в ячейке по адресу слева, а второй — в ячейке, следующей за ней. Эти три строки эквивалентны следующей строке.

1000h: 00h, A7h, 00h, 1Ch, 00h, 00h

Таким образом, программа для умножения двух чисел может быть записана в виде следующей серии команд.

0000h: LOD A, [1005h]

ADD A, [1001h]

STO [1005h], A

LOD A, [1004h]

ADC A, [1000h]

STO [1004h], A

LOD A, [1003h]

ADD A, [001Eh]

STO [1003h], A

JNZ 0000h

001Eh: HLT

1000h: 00h, A7h

1002h: 00h, 1Ch

1004h: 00h, 00h

Пустые строки и другие пробелы применяются для упрощения восприятия. При написании кода лучше не использовать фактические адреса, поскольку они могут измениться. Например, если вы решили сохранить числа в ячейках с 2000h до 20005h, то придется переписывать множество выражений. Лучше использовать метки для обозначения ячеек памяти. Эти метки — просто слова или то, что их напоминает.

BEGIN: LOD A,[RESULT + 1]

ADD A,[NUM1 + 1]

STO [RESULT + 1], A

LOD A,[RESULT]

ADC A,[NUM1]

STO [RESULT], A

LOD A,[NUM2 + 1]

ADD A,[NEG1]

STO [NUM2 + 1], A

JNZ BEGIN

NEG1: HLT

NUM1: 00h, A7h

NUM2: 00h, 1Ch

RESULT: 00h, 00h

Важно: метки NUM1, NUM2 и RESULT ссылаются на ячейки памяти, где хранятся два байта. В приведенных выражениях метки NUM1 + 1, NUM2 + 1 и RESULT + 1 ссылаются на второй байт соответствующей метки. Обратите внимание на метку NEG1 (negative one, то есть «минус один») у команды HLT.

Наконец, чтобы не забыть, в чем заключается смысл того или иного выражения, вы можете добавить небольшие комментарии на естественном языке, отделив их от кода точкой с запятой.

BEGIN: LOD A,[RESULT + 1]

ADD A,[NUM1 + 1]; Прибавить младший байт

STO [RESULT + 1], A

LOD A,[RESULT]

ADC A,[NUM1]; Прибавить старший байт

STO [RESULT], A

LOD A,[NUM2 + 1]

ADD A,[NEG1]; Уменьшить второе число на 1

STO [NUM2 + 1], A

JNZ BEGIN

NEG1: HLT

NUM1: 00h, A7h

NUM2: 00h, 1Ch

RESULT: 00h, 00h

Продемонстрированный язык компьютерного программирования называется языком ассемблера. Этот язык — некий компромисс между машинным кодом, состоящим исключительно из цифр, и многословными описаниями команд на человеческом языке, сопровождаемый адресами ячеек, выраженными в символьной форме. Люди иногда путают машинный код с языком ассемблера, поскольку это просто два разных способа выражения одного и того же. Каждый оператор на языке ассемблера соответствует определенным байтам машинного кода.

Если бы вы решили создать программу для собранного в этой главе компьютера, вероятно, сначала нужно было бы записать ее на бумаге на языке ассемблера. Когда сочтете, что программа написана правильно и готова к тестированию, вы вручную преобразуете каждый фрагмент кода на ассемблере в машинный код. После этого вы сможете использовать переключатели, чтобы ввести машинный код в массив RAM и запустить программу, то есть позволить ей выполнить набор введенных команд.

Изучение концепций компьютерного программирования предполагает знакомство с понятием ошибки. При кодировании, особенно при написании машинного кода, легко ошибиться. Ввод неправильного числа уже гарантирует возникновение неприятностей, но что произойдет, если неправильно ввести код команды? Если вы введете значение 11h (команда «Сохранить») вместо 10h (команда «Загрузить»), то машина не только не загрузит число, которое должна, но само это число будет заменено значением, которое в настоящий момент хранится в аккумуляторе. Некоторые ошибки могут приводить к непредсказуемым результатам. Предположим, вы используете команду «Перейти» для перехода к ячейке, которая не содержит действительного кода команды. Или, допустим, вы случайно применили команду «Сохранить», записав число в ячейке с кодом команды. Все может случиться (и частенько случается).

Даже в моей программе для умножения есть ошибка. Если вы запустите ее дважды, то при втором выполнении машина умножит A7h на 256 и прибавит произведение к уже полученному результату. Это связано с тем, что после первого выполнения программы в ячейке 1003h будет находиться значение 0. Когда вы запустите программу во второй раз, к этому значению будет прибавлено число FFh. Значение в ячейке 1003h будет отлично от 0, поэтому программа будет продолжать работу до тех пор, пока значение в этой ячейке не станет равным 0.

Наша машина может выполнять умножение, однако аналогичным образом она может выполнять и деление. Ранее я утверждал, что такая машина может использовать эти примитивные функции для вычисления квадратных корней, логарифмов и тригонометрических функций. Все, что ей требуется, — это оборудование для сложения и вычитания, а также способ реализации условного перехода для выполнения нужного кода. Любой программист скажет, что все остальное можно сделать с помощью программного обеспечения.

Разумеется, это программное обеспечение может быть довольно сложным. Существуют целые книги с описанием алгоритмов, используемых программистами для решения конкретных задач. К обсуждению этой темы мы пока не готовы. Ранее мы работали с целыми числами и не касались вопроса представления десятичных дробей в компьютере. Об этом поговорим в главе 23.

Я уже несколько раз упоминал, что все компоненты для создания таких устройств появились более ста лет назад. Однако компьютер, описанный в этой главе, вряд ли мог быть собран в то время. Многие из концепций, использованных в его конструкции, не были очевидны и в середине 1930-х годов, когда разрабатывались первые релейные компьютеры. Их начали осознавать примерно в 1945 году. До этого люди все еще пытались создавать компьютеры на основе десятичных, а не двоичных чисел. Кроме того, компьютерные программы не всегда хранились в памяти — иногда они были закодированы на бумажной ленте. На заре компьютерной эры память была очень дорогой и громоздкой. Создание массива RAM емкостью 64 килобайт из пяти миллионов телеграфных реле казалось такой же абсурдной идеей, как и сейчас.

Пришло время рассмотреть все, что мы узнали, в контексте истории развития вычислений и вычислительной техники. Возможно, мы обнаружим, что нам не придется собирать этот сложный релейный компьютер. Как я упоминал в главе 12, на смену реле пришли такие электронные устройства, как вакуумные лампы и транзисторы. Вероятно, мы также найдем, что кто-то уже создал устройство, эквивалентное нашему процессору и памяти, умещающееся на ладони.