Привет, Хабр. В рамках нового набора на курс «Электроника и электротехника» подготовили перевод нового увлекательного разбора от Ken Shirrif.
В 1980-е годы, если вы хотели ускорить работу своего IBM PC, можно было приобрести сопроцессор Intel 8087 для вычислений с плавающей точкой. С этим чипом системы автоматизированного проектирования (CAD), электронные таблицы, авиационные симуляторы и другие программы работали значительно быстрее. Чип 8087, разумеется, умел выполнять сложение, вычитание, умножение и деление, но помимо этого мог вычислять трансцендентные функции, такие как тангенс и логарифмы, а также предоставлять константы, например π. В общей сложности 8087 добавлял компьютеру 62 новые инструкции.
Но как ПК определяет, является ли инструкция командой для вычислений с плавающей точкой, предназначенной для 8087, или это обычная инструкция для процессора 8086 или 8088? И как сам чип 8087 интерпретирует инструкции, чтобы понять их смысл? Оказывается, декодирование инструкций внутри 8087 устроено гораздо сложнее, чем можно было бы предположить. В 8087 используется сразу несколько подходов, а схемы декодирования распределены по всему кристаллу. В этой статье я объясню, как работают эти схемы.
Чтобы выполнить реверс-инжиниринг 8087, я вскрыл керамический корпус чипа и сделал множество снимков кремниевого кристалла под микроскопом. Сложные узоры на кристалле образованы металлическими соединениями, а также слоями поликремния и кремния под ними. Нижняя половина чипа — это тракт обработка данных, схема, выполняющая вычисления с 80-битными числами с плавающей точкой. Слева от тракта данных находится ПЗУ констант, в котором хранятся важные значения, такие как π. Справа расположены восемь регистров, которые программист использует для хранения чисел с плавающей точкой; в ��еобычном дизайнерском решении эти регистры организованы в виде стека. Числа с плавающей точкой охватывают огромный диапазон значений, представляя число через мантиссу и показатель степени; в 8087 для обработки мантиссы и показателя степени предусмотрены отдельные схемы.

Инструкции чипа определяются большим ПЗУ микрокода, расположенным в центре.(примеч.1) Чтобы выполнить инструкцию, 8087 сначала декодирует её, после чего микрокодный движок (microcode engine) начинает исполнять соответствующие микроинструкции из ПЗУ микрокода. В правой верхней части кристалла расположен блок интерфейса шины — Bus Interface Unit (BIU), который взаимодействует с основным процессором и памятью через системную шину компьютера. В целом BIU и остальная часть чипа работают независимо, однако, как мы увидим далее, BIU играет важную роль в декодировании и выполнении инструкций.
Взаимодействие с основным процессором 8086/8088
Чип 8087 выступал в роли сопроцессора по отношению к основному процессору 8086 (или 8088). Когда встречалась инструкция с плавающей точкой, 8086 передавал её выполнение сопроцессору 8087. Но как именно 8086 и 8087 определяют, какой из них должен выполнять конкретную инструкцию? Можно было бы предположить, что 8086 напрямую сообщает 8087, когда тому нужно выполнять команду, однако на практике это взаимодействие устроено гораздо сложнее.
В процессоре 8086 есть восемь кодов операций, зарезервированных для сопроцессора — они называются ESCAPE-опкодами. 8087 определяет, какую инструкцию выполняет 8086, наблюдая за шиной — эту задачу выполняет BIU (Bus Interface Unit, блок интерфейса шины).(примеч.2) Если инструкция является ESCAPE, значит она предназначена для 8087. Однако возникает проблема: у 8087 нет доступа к регистрам 8086 (и наоборот), поэтому единственный способ обмена данными — через память. Но 8086 обращается к памяти с использованием сложной схемы, включающей регистры смещения и сегментные регистры. Как 8087 может определить нужный адрес памяти, если у него нет доступа к этим регистрам?
Решение состоит в следующем. Когда встречается ESCAPE-инструкция, процессор 8086 начинает её выполнять, несмотря на то что она предназначена для 8087. Он вычисляет адрес памяти, к которому обращается инструкция, и выполняет чтение по этому адресу, но игнорирует полученные данные. В это время 8087 наблюдает за шиной памяти, определяет, к какому адресу происходит обращение, и сохраняет этот адрес во внутреннем регистре BIU. Когда 8087 приступает к выполнению инструкции, он использует этот адрес для чтения и записи в память. По сути, 8087 делегирует вычисление адреса процессору 8086.
Структура инструкций 8087
Чтобы понять инструкции 8087, необходимо внимательнее рассмотреть структуру инструкций процессора 8086. Особенно важен так называемый байт ModR/M, поскольку все инструкции 8087 используют его.
В 8086 применяется достаточно сложная система кодов операций, включающая однобайтные опкоды, префиксы и более длинные инструкции. Примерно четверть опкодов использует второй байт — ModR/M, который задаёт используемые регистры и/или адрес памяти через довольно запутанное кодирование. Например, адрес памяти может вычисляться как сумма регистров BX и SI или как значение регистра BP плюс двухбайтовое смещение. Первые два бита байта ModR/M — это биты «MOD». При обращении к памяти биты MOD указывают, сколько байтов смещения адреса следует за байтом ModR/M (0, 1 или 2), а биты «R/M» определяют способ вычисления адреса. Значение MOD, равное 3, означает, что инструкция работает с регистрами и не обращается к памяти.

На диаграмме выше показано, что инструкция 8087 состоит из ESCAPE-опкода, за которым следует байт ModR/M. ESCAPE-опкод определяется специальным битовым шаблоном 11011, при этом три оставшихся бита (выделены зелёным) в первом байте используются для задания типа инструкции 8087. Как уже упоминалось, байт ModR/M имеет два варианта формы. Первая форма используется для обращения к памяти: в этом случае биты MOD имеют значения 00, 01 или 10, а биты R/M определяют способ вычисления адреса памяти. При этом остаётс�� три бита (зелёные) для указания адреса. Вторая форма используется для внутренних операций без обращения к памяти: в этом случае биты MOD равны 11. Поскольку биты R/M в этой форме не используются, в байте R/M доступно шесть бит (зелёные) для задания инструкции.
Основная задача разработчиков 8087 заключалась в том, чтобы уместить все инструкции в доступные биты таким образом, чтобы декодирование оставалось относительно простым. На диаграмме ниже показано несколько инструкций 8087, иллюстрирующих, как это реализовано. Первые три инструкции выполняются внутри чипа, поэтому у них биты MOD равны 11; зелёные биты определяют конкретную инструкцию. Инструкция сложения устроена сложнее, поскольку может работать как с памятью (первая форма), так и с регистрами (вторая форма) в зависимости от битов MOD. Четыре бита, выделенные ярко-зелёным (0000), одинаковы для всех инструкций ADD; инструкции вычитания, умножения и деления имеют ту же структуру, но отличаются значениями тёмно-зелёных битов. Например, значение 0001 соответствует умножению, а 0100 — вычитанию. Остальные зелёные биты (MF, d и P) задают варианты инструкции сложения, определяя формат данных, направление операции и извлечение значения из стека по завершении. Последние три бита выбирают режим адресации R/M для операций с памятью или регистр стека ST(i) для операций с регистрами.

Выбор микрокодовой процедуры
Большинство инструкций 8087 реализованы с помощью микрокода: выполнение каждой инструкции разбивается на последовательность низкоуровневых микроинструкций. В чипе 8087 есть микрокодный движок; его можно рассматривать как мини-процессор, который управляет работой 8087, выполняя микрокодовую процедуру — по одной микроинструкции за шаг. Микрокодный движок формирует 11-битный микроадрес для ПЗУ, указывая, какую микроинструкцию нужно выполнить. Обычно выполнение микрокода идёт последовательно, однако поддерживаются и условные переходы, и вызовы подпрограмм.
Но как микрокодный движок понимает, с какого места начинать выполнение для конкретной машинной инструкции? В теории можно было бы подавать код операции инструкции прямо в ПЗУ, которое возвращало бы начальный микроадрес. Однако это решение оказалось бы непрактичным, так как для декодирования 11-битного опкода потребовалось бы ПЗУ на 2048 слов.(примеч.3) (Сегодня ПЗУ на 2K кажется небольшим, но на тот момент это был значительный объём; ПЗУ микрокода 8087 едва уместилось в 1648 слов.) Вместо этого в 8087 используется более эффективная, хотя и более сложная, система декодирования инструкций, построенная на комбинации логических элементов и программируемых логических матриц (Programmable Logic Array, PLA). Эта система содержит 22 точки входа в микрокод, что значительно практичнее, чем 2048.
Процессоры часто используют схему под названием PLA как часть механизма декодирования инструкций. Идея PLA заключается в том, чтобы обеспечить компактный и гибкий способ реализации произвольных логических функций. Любую булеву функцию можно представить в виде «суммы произведений» — набора операций И (произведений), объединённых операцией ИЛИ (суммированием). В PLA есть блок, называемый плоскостью И (AND plane), который формирует необходимые произведения. Выходы этой плоскости подаются на второй блок — плоскость ИЛИ (OR plane), где они объединяются. Физически PLA реализуется в виде сетки, где в каждой точке может либо присутствовать транзистор, либо отсутствовать. Изменяя конфигурацию транзисторов, можно реализовать нужную логическую функцию.

PLA может реализовывать произвольную логику, однако в 8087 такие матрицы часто используются как оптимизированные ПЗУ.(примеч.4) Плоскость И (AND plane) сопоставляет битовые шаблоны,(примеч.5) выбирая соответствующую запись из плоскости ИЛИ (OR plane), в которой хранятся выходные значения — микроадрес для каждой процедуры. Преимущество PLA по сравнению с обычным ПЗУ заключается в том, что один столбец выхода может использоваться для множества различных входов, что уменьшает размер схемы.
На изображении ниже показана часть PLA, отвечающей за декодирование инструкций.(примеч.6) Горизонтальные входные линии — это проводники из поликремния, расположенные поверх кремния. Розоватые области — это легированный кремний. Когда поликремний пересекает легированный кремний, образуется транзистор (обозначен зелёным). Там, где в легированном кремнии есть разрыв, транзистора нет (обозначено красным). (Выходные линии проходят вертикально, но здесь не видны: металлический слой был удалён, чтобы показать структуру кремния под ним.) Если линия поликремния активируется, она включает все транзисторы в своей строке, подтягивая соответствующие выходные столбцы к земле. (Если ни один транзистор не включён, подтягивающий транзистор удерживает выход в высоком состоянии.) Таким образом, рисунок областей легированного кремния формирует в PLA сетку транзисторов, реализующую заданную логическую функцию.(примеч.7)

Стандартный способ декодирования инструкций с помощью PLA — подавать на вход биты инструкции (и их инверсии). PLA затем сопоставляет их с заданными битовыми шаблонами. Однако в 8087 используется дополнительная предварительная обработка, позволяющая уменьшить размер PLA. Например, биты MOD обрабатываются так, чтобы формировался сигнал, если они равны 0, 1 или 2 (то есть операция с памятью), и отдельный сигнал — если они равны 3 (то есть операция с регистрами). Это позволяет обрабатывать случаи 0, 1 и 2 с помощью одного шаблона в PLA. Другой сигнал указывает, что старшие биты имеют вид 001 111xxxxx; это означает, что поле R/M участвует в выборе инструкции.(примеч.8) Иногда выход PLA подаётся обратно на вход, чтобы исключить уже распознанную группу инструкций из другой группы. Все эти приёмы уменьшают размер PLA ценой добавления некоторого количества логических элементов.
В результате работы плоскости И декодирующей PLA формируется 22 сигнала, каждый из которых соответствует отдельной инструкции или группе инструкций с общей точкой входа в микрокод. Нижняя часть этой PLA действует как ПЗУ, в котором хранятся 22 точки входа в микрокод и из которого выбирается нужная.(примеч.9)
Декодирование инструкций внутри микрокода
Многие инструкции 8087 используют одни и те же микрокодовые процедуры. Например, инструкции сложения, вычитания, умножения, деления, обратного вычитания и обратного деления все переходят к одной и той же процедуре микрокода. Это уменьшает размер микрокода, поскольку эти инструкции используют общий код для подготовки операции и обработки результата. Однако в какой-то момент микрокод должен разветвляться, чтобы выполнить конкретное действие. Кроме того, некоторые арифметические опкоды обращаются к вершине стека, другие — к произвольному элементу стека, некоторые работают с памятью, а некоторые меняют порядок операндов, что требует различных действий на уровне микрокода. Как же микрокод выполняет разные действия для разных опкодов, оставаясь при этом общим?
Секрет в том, что микрокодный движок 8087 поддерживае�� условные вызовы подпрограмм, возвраты и переходы, основанные на 49 различных условиях (подробности). В частности, пятнадцать условий анализируют саму инструкцию. Некоторые условия проверяют конкретные битовые шаблоны, например переход, если установлен младший бит, или более сложные случаи, такие как соответствие опкода шаблону 0xx 11xxxxxx. Другие условия определяют конкретные инструкции, например FMUL. В результате микрокод может выбирать разные пути выполнения для разных инструкций. Например, обратное вычитание или обратное деление реализуются в микрокоде путём проверки инструкции и при необходимости перестановки операндов, при этом остальная часть кода остаётся общей.
В микрокоде также предусмотрена специальная цель перехода, реализующая трёхветвевой переход в зависимости от текущей выполняемой машинной инструкции. В блоке микрокода есть ПЗУ переходов, содержащее 22 точки входа для переходов и вызовов подпрограмм.(примеч.10) Однако переход к цели 0 использует специальную логику: вместо неё происходит переход к цели 1 для инструкции умножения, к цели 2 для сложения/вычитания или к цели 3 для деления. Этот особый механизм реализован с помощью логических элементов в правом верхнем углу декодера переходов.

Жёстко реализованная обработка инструкций
Некоторые инструкции 8087 реализованы напрямую аппаратно в блоке интерфейса шины (BIU), без использования микрокода. Например, инструкции включения и отключения прерываний, а также сохранения и восстановления состояния реализованы аппаратно. Декодирование этих инструкций выполняется отдельной схемой, отличной от описанного ранее декодера инструкций.
На первом этапе небольшая PLA декодирует верхние 5 бит инструкции. Самое важное — если эти биты равны 11011, это означает ESCAPE-инструкцию, то есть начало операции 8087. Это заставляет 8087 начать интерпретацию инструкции и сохраняет опкод во внутреннем регистре BIU для последующего использования декодером инструкций. Вторая небольшая PLA принимает выходы первой (для верхних 5 бит) и объединяет их с младшими тремя битами. Она декодирует конкретные значения инструкций: D9, DB, DD, E0, E1, E2 или E3. Первые три значения соответствуют определённым ESCAPE-инструкциям и сохраняются в защёлках.
Две PLA аналогичным образом декодируют второй байт. Логические элементы объединяют выходы PLA для второго байта с защёлкнутыми значениями первого байта, в результате чего распознаются одиннадцать инструкций, реализованных аппаратно.(примеч.11) Некоторые из этих инструкций работают напрямую с регистрами, например очищают флаги исключений; сигнал декодированной инструкции поступает в соответствующий регистр и напрямую изменяет его.(примеч.12) Другие аппаратно реализованные инструкции более сложны: они записывают состояние чипа в память или считывают его из памяти. Такие инструкции требуют нескольких операций с памятью, которыми управляет автомат состояний блока интерфейса шины. Для каждой из этих инструкций используется триггер, который активируется при декодировании инструкции и отслеживает, какая именно инструкция сейчас выполняется.
Для инструкций сохранения и восстановления состояния 8087 (FSAVE и FRSTOR) возникает дополнительная сложность. Эти инструкции частично реализованы в BIU, который переносит соответствующие регистры BIU в память или из памяти. Однако затем выполнение передаётся микрокоду, где соответствующая процедура сохраняет или загружает регистры с плавающей точкой. Переход к этой микрокодовой процедуре осуществляется не через обычную схему переходов микрокода. Вместо этого используются два жёстко заданных значения, которые принудительно устанавливают микроадрес на процедуру сохранения или восстановления.(примеч.13)
Константы
В 8087 есть семь инструкций для загрузки констант с плавающей точкой, таких как π, 1 или log10(2). В чипе предусмотрено ПЗУ констант, где хранятся эти значения, а также константы для трансцендентных операций. Можно было бы ожидать, что 8087 просто загружает нужную константу из этого ПЗУ, используя инструкцию для её выбора. Однако на практике всё устроено значительно сложнее.(примеч.14)
Анализ ПЗУ декодирования инструкций показывает, что разные константы реализованы через разные микрокодовые процедуры: инструкции загрузки констант FLDLG2 и FLDLN2 имеют одну точку входа; FLD1, FLD2E, FLDL2T и FLDPI — другую; а FLDZ (ноль) — третью. Понятно, что ноль является особым случаем, но возникает вопрос: почему для остальных констант используются две разные процедуры?
Объяснение заключается в том, что дробная часть каждой константы хранится в ПЗУ констант, а показатель степени — в отдельном, более компактном ПЗУ. Чтобы уменьшить размер ПЗУ показателей степени, в нём хранятся только некоторые необходимые значения. Если для константы требуется показатель степени на единицу больше, чем есть в ПЗУ, микрокод прибавляет единицу к значению из ПЗУ, вычисляя показатель степени «на лету».
Таким образом, инструкции загрузки констант используют три отдельных механизма декодирования. Сначала ПЗУ декодирования инструкций определяет подходящую микрокодовую процедуру для данной инструкции. Затем PLA констант декодирует инструкцию и выбирает нужную константу. Наконец, микрокодовая процедура проверяет младший бит инструкции и при необходимости увеличивает показатель степени.
Выводы
В завершение рассмотрения схем декодирования на диаграмме ниже показано, как различные блоки расположены на кристалле. На изображении представлена верхняя правая часть кристалла: микрокодный движок находится слева, а часть ПЗУ — внизу.

Архитектуру 8087 нельзя назвать «чистой» — она содержит множество специализированных решений и частных случаев. Декодирование инструкций в 8087 хорошо это иллюстрирует. Уже сама структура инструкций 8086 и наличие байта ModR/M делают декодирование сложным. Поверх этого в 8087 добавляется многоуровневая система: PLA для декодирования инструкций, условные переходы в микрокоде, зависящие от инструкции, специальная цель перехода, также зависящая от инструкции, выбор констант на основе инструкции и отдельные инструкции, декодируемые в BIU.
У такой сложной архитектуры 8087 была причина: на тот момент чип находился на пределе технологических возможностей, поэтому разработчикам приходилось использовать любые приёмы, позволяющие уменьшить его размер. Если реализация какого-либо частного случая позволяла сэкономить несколько транзисторов или немного сократить объём ПЗУ микрокода, это считалось оправданным. Тем не менее, на ранних этапах 8087 едва поддавался производству: первоначальный выход годных кристаллов составлял всего два рабочих чипа на одну кремниевую пластину. Несмотря на столь сложный старт, стандарт вычислений с плавающей точкой, основанный на 8087, сегодня используется практически в каждом процессоре.
Примечания и ссылки (осторожно, много текста)
1. Содержимое ПЗУ микрокода доступно на github; частичная расшифровка выполнена благодаря Smartest Blob.
2. Для 8087 сложно определить, что именно делает 8086, поскольку 8086 выполняет предварительную выборку инструкций. В результате инструкция, замеченная на шине, может быть выполнена позже или вообще отброшена. Чтобы понять, какая инструкция выполняется в данный момент, сопроцессор 8087 дублирует очередь инструкций процессора 8086. 8087 наблюдает за шиной памяти и копирует все предварительно загруженные инструкции. Поскольку 8087 не может по шине определить, когда 8086 начинает выполнение новой инструкции или очищает очередь при переходе на новый адрес, процессор 8086 передаёт 8087 два сигнала состояния очереди. С их помощью 8087 точно знает, какую инструкцию выполняет 8086.
Очередь инструкций 8087 содержит шесть 8-битных регистров, как и у 8086. Однако два последних регистра в 8087 соединены между собой, поэтому фактически используются только пять. По моей гипотезе, поскольку 8087 копирует активную инструкцию в отдельные регистры (в отличие от 8086), пяти регистров достаточно. Возникает вопрос, почему лишний регистр не был удалён с кристалла, чтобы не тратить ценное место.
Процессор 8088, использовавшийся в IBM PC, имеет очередь длиной четыре байта вместо шести. Он почти идентичен 8086, за исключением того, что использует 8-битную шину памяти вместо 16-битной. Из-за более узкой шины механизм предварительной выборки чаще мешает другим обращениям к памяти, поэтому была реализована более короткая очередь.
Знание размера очереди критически важно для сопроцессора 8087. Для этого при запуске системы специаль��ый сигнал позволяет 8087 определить, подключён ли к нему процессор 8086 или 8088.
3. Рассматриваемая часть опкода составляет 11 бит: верхние 5 бит всегда равны 11011 для ESCAPE-опкода, поэтому их можно игнорировать при декодировании. В блоке интерфейса шины (BIU) есть 3-битный регистр для хранения первого байта инструкции и 8-битный регистр для хранения второго байта. Регистры BIU имеют неоднородную структуру, поскольку используются 3-битные, 8-битные и 10-битные регистры (последние хранят половину 20-битного адреса).
4. В чём разница между PLA и ПЗУ? Между ними существует значительное пересечение: ПЗУ может заменить PLA, а PLA, в свою очередь, может реализовывать функциональность ПЗУ. По сути, ПЗУ — это PLA, у которого первый этап представляет собой двоичный декодер, поэтому в ПЗУ каждой комбинации входных значений соответствует отдельная строка. Однако первый этап ПЗУ можно оптимизировать так, чтобы разные входные значения приводили к одному и тому же выходу; возникает вопрос — это всё ещё ПЗУ или уже PLA?
«Формальное» различие заключается в том, что в ПЗУ в каждый момент времени активируется только одна строка, тогда как в PLA могут активироваться сразу несколько строк, и их выходные значения объединяются. (Поэтому из ПЗУ значения считывать проще, а из PLA — сложнее.)
Я считаю, что PLA для декодирования инструкций в данном случае лучше всего описывать как конструкцию, где первый этап — это PLA, а второй работает как ПЗУ. Можно также назвать её частично декодированным ПЗУ или просто PLA. Надеюсь, такая терминология не вызывает путаницы.
5. Чтобы сопоставить битовый шаблон инструкции, на вход PLA подаются биты инструкции вместе с их инверсиями; это позволяет сопоставлять как с нулевыми, так и с единичными значениями. Каждая строка PLA соответствует определённому шаблону: битам, которые должны быть равны 1, битам, которые должны быть равны 0, и битам, которые не имеют значения. Если опкоды распределены рационально, небольшого количества таких шаблонов достаточно, чтобы покрыть все инструкции, что уменьшает размер декодера.
Возможно, это сравнение несколько натянуто, но PLA во многом напоминает нейронную сеть. Каждый столбец в плоскости И похож на нейрон, который «срабатывает» при распознавании определённого входного шаблона. Плоскость ИЛИ выступает как второй слой, объединяющий сигналы первого. Однако «веса» в PLA фиксированы — это либо 0, либо 1, поэтому она не обладает гибкостью настоящей нейросети.
6. PLA для декодирования инструкций имеет необычную компоновку: вторая плоскость повернута на 90°. В обычной PLA (слева) входы (красные линии) поступают в первую плоскость, перпендикулярные выходы из неё (фиолетовые) подаются во вторую плоскость, а итоговые выходы (синие) идут параллельно входам. В адресной PLA вторая плоскость повернута на 90°, поэтому выходы располагаются перпендикулярно входам. Такой подход требует дополнительной разводки (горизонтальные фиолетовые линии), но, вероятно, лучше подошёл для 8087, поскольку выходы выровнены с остальной частью блока микрокода.

7. Если рассмотреть реализацию PLA подробнее, то транзисторы в каждой строке плоскости И образуют логический элемент ИЛИ-НЕ (NOR), поскольку при включении любого транзистора выход притягивается к низкому уровню. Аналогично, транзисторы в каждом столбце плоскости ИЛИ также формируют элемент ИЛИ-НЕ. Тогда возникает вопрос: почему PLA описывается как состоящая из плоскости И и плоскости ИЛИ, а не из двух плоскостей ИЛИ-НЕ?
Ответ связан с законом де Моргана: логические выражения в форме NOR-NOR можно рассматривать как эквивалентные выражениям AND-OR (с инверсией входов и выходов). В таком виде логику гораздо проще воспринимать — как набор операций И, объединённых операцией ИЛИ.
Обратный вопрос: почему PLA не строят непосредственно из элементов И и ИЛИ? Причина в том, что такие элементы сложнее реализовать на NMOS-транзисторах, поскольку требуется добавлять явные инверторы. Кроме того, в NMOS-схемах элементы ИЛИ-НЕ обычно работают быстрее, чем И-НЕ (NAND), так как транзисторы соединены параллельно. (В CMOS ситуация обратная: NAND быстрее, поскольку более слабые PMOS-транзисторы соединены параллельно.)
8. Опкоды 8087 можно организовать в виде таблиц, которые отражают их внутреннюю структуру. (В каждой таблице координата строки (Y) задаётся младшими 3 битами первого байта, а координата столбца (X) — тремя битами после MOD-битов во втором байте.)
Операции с памятью используют следующую кодировку при MOD = 0, 1 или 2. Каждая ячейка соответствует 8 различным режимам адресации.
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
|---|---|---|---|---|---|---|---|---|
0 | FADD | FMUL | FCOM | FCOMP | FSUB | FSUBR | FDIV | FDIVR |
1 | FLD |
| FST | FSTP | FLDENV | FLDCW | FSTENV | FSTCW |
2 | FIADD | FIMUL | FICOM | FICOMP | FISUB | FISUBR | FIDIV | FIDIVR |
3 | FILD |
| FIST | FISTP |
| FLD |
| FSTP |
4 | FADD | FMUL | FCOM | FCOMP | FSUB | FSUBR | FDIV | FDIVR |
5 | FLD |
| FST | FSTP | FRSTOR |
| FSAVE | FSTSW |
6 | FIADD | FIMUL | FICOM | FICOMP | FISUB | FISUBR | FIDIV | FIDIVR |
7 | FILD |
| FIST | FISTP | FBLD | FILD | FBSTP | FISTP |
Ключевой момент в том, что кодирование инструкций обладает высокой регулярностью, что упрощает процесс декодирования. Например, базовые арифметические операции (от FADD до FDIVR) повторяются через строку. Однако в таблице присутствуют и заметные нерегулярности, которые усложняют декодирование.
Операции с регистрами (MOD = 3) имеют схожую структуру, но в них нерегулярностей ещё больше.
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
|---|---|---|---|---|---|---|---|---|
0 | FADD | FMUL | FCOM | FCOMP | FSUB | FSUBR | FDIV | FDIVR |
1 | FLD | FXCH | FNOP |
| misc1 | misc2 | misc3 | misc4 |
2 |
|
|
|
|
|
|
|
|
3 |
|
|
|
| misc5 |
|
|
|
4 | FADD | FMUL |
|
| FSUB | FSUBR | FDIV | FDIVR |
5 | FFREE |
| FST | FSTP |
|
|
|
|
6 | FADDP | FMULP |
| FCOMPP | FSUBP | FSUBRP | FDIVP | FDIVRP |
7 |
|
|
|
|
|
|
|
|
В большинстве случаев каждая ячейка соответствует 8 различным значениям регистра стека, однако есть исключения. Инструкции NOP и FCOMPP имеют по одному опкоду, «занимая» всю ячейку и не используя остальные возможные значения.
Пять ячеек в таблице кодируют не номера регистров, а сразу несколько инструкций. Первые четыре (выделенные красным) содержат так называемые служебные инструкции, обрабатываемые PLA декодирования:
misc1 = FCHS, FABS, FTST, FXAM
misc2 = FLD1, FLDL2T, FLDL2E, FLDPI, FLDLG2, FLDLN2, FLDZ (инструкции загрузки констант)
misc3 = F2XM1, FYL2X, FPTAN, FPATAN, FXTRACT, FDECSTP, FINCSTP
misc4 = FPREM, FYL2XP1, FSQRT, FRNDINT, FSCALE
Последняя служебная ячейка (выделена жёлтым) содержит инструкции, обрабатываемые блоком интерфейса шины (BIU):
misc5 = FENI, FDISI, FCLEX, FINIT
Любопытно, что опкоды 8087 (как и у 8086) гораздо лучше воспринимаются в восьмеричной системе счисления, чем в шестнадцатеричной. В восьмеричной записи опкод 8087 имеет простой вид: 33Y MXR, где X и Y — координаты в приведённой выше таблице, M — значение MOD (0, 1, 2 или 3), а R — поле R/M или номер регистра стека.
9. 22 выхода PLA декодера инструкций соответствуют следующим группам инструкций, активируя одну строку ПЗУ и формируя соответствующий микроадрес. Из этой таблицы видно, какие инструкции объединены в микрокоде:
0 #0200 FXCH 1 #0597 FSTP (BCD) 2 #0808 FCOM FCOMP FCOMPP 3 #1008 FLDLG2 FLDLN2 4 #1527 FSQRT 5 #1586 FPREM 6 #1138 FPATAN 7 #1039 FPTAN 8 #0900 F2XM1 9 #1020 FLDZ 10 #0710 FRNDINT 11 #1463 FDECSTP FINCSTP 12 #0812 FTST 13 #0892 FABS FCHS 14 #0065 FFREE FLD 15 #0217 FNOP FST FSTP (not BCD) 16 #0001 FADD FDIV FDIVR FMUL FSUB FSUBR 17 #0748 FSCALE 18 #1028 FXTRACT 19 #1257 FYL2X FYL2XP1 20 #1003 FLD1 FLDL2E FLDL2T FLDPI 21 #1468 FXAM
10. PLA декодера инструкций содержит 22 записи, и таблица переходов также содержит 22 записи. Это совпадение, а не закономерность.
Запись в ПЗУ таблицы переходов выбирается по пяти битам микроинструкции. ПЗУ организовано так, что на каждую строку приходится два 11-битных слова, расположенных вперемешку. (То, что получается 22 бита, тоже является совпадением.) Старшие четыре бита номера перехода выбирают строку в ПЗУ, а младший бит — одно из двух слов в этой строке.
Эта схема модифицирована для цели 0 — трёхветвевого перехода. Первая строка ПЗУ выбирается для цели 0, если текущая инструкция — умножение, либо для цели 1. Вторая строка выбирается для цели 0, если текущая инструкция — сложение или вычитание, либо для цели 2. Третья строка выбирается для цели 0, если выполняется деление, либо для цели 3. Таким образом, цель 0 фактически приводит к выбору строк 1, 2 или 3. Однако нужно помнить, что в каждой строке содержится два слова, и выбор между ними определяется младшим битом номера перехода. Возникает проблема: при цели 0 и операции умножения выбирается левое слово строки 1, тогда как цель 1 выбирает правое слово той же строки, хотя оба должны давать одинаковый адрес. Решение заключается в том, что в строках 1, 2 и 3 один и тот же адрес записан дважды, поэтому в каждой из этих строк одно значение фактически «теряется».
Для справки, содержимое таблицы переходов:
0: переход к цели 1 для FMUL, к 2 для FADD/FSUB/FSUBR, к 3 для FDIV/FDIVR 1: #0359 2: #0232 3: #0410 4: #0083 5: #1484 6: #0122 7: #0173 8: #0439 9: #0655 10: #0534 11: #0299 12: #1572 13: #1446 14: #0859 15: #0396 16: #0318 17: #0380 18: #0779 19: #0868 20: #0522 21: #0801
11. Одиннадцать инструкций реализованы непосредственно аппаратно в блоке BIU. Четыре из них относительно простые — они устанавливают или сбрасывают биты:
- FINIT (инициализация),
- FENI (включение прерываний),
- FDISI (отключение прерываний)
- и FCLEX (сброс исключений).
Ещё шесть инструкций более сложны — они сохраняют состояние в память или загружают его из памяти:
- FLDCW (загрузка управляющего слова),
- FSTCW (сохранение управляющего слова),
- FSTSW (сохранение слова состояния),
- FSTENV (сохранение окружения),
- FLDENV (загрузка окружения),
- FSAVE (сохранение состояния)
- и FRSTOR (восстановление состояния).
Как упоминалось ранее, две последние инструкции частично реализованы в микрокоде.
12. Даже на первый взгляд простая инструкция требует больше схем, чем можно ожидать. Например, после декодирования инструкции FCLEX (сброс исключений) сигнал проходит через девять логических элементов, прежде чем очистить биты исключений в регистре состояния. По пути он проходит через триггер для синхронизации, логический элемент для объединения с сигналом сброса и различные инверторы и драйверы. Несмотря на то что такие инструкции выглядят мгновенными, на практике они обычно занимают около 5 тактов из-за внутренних накладных расходов 8087.
13. Я приведу дополнительные подробности о схеме, выполняющей переход к микрокоду сохранения или восстановления состояния. Блок интерфейса шины (BIU) отправляет два сигнала в микрокодный движок: один — для перехода к коду сохранения, другой — к коду восстановления. Эти сигналы буферизуются и задерживаются с помощью конденсатора, вероятно для точной настройки их временных характеристик.
В блоке микрокода, непосредственно над таблицей переходов, находятся два жёстко заданных значения для соответствующих процедур; сигнал от BIU приводит к тому, что нужное з��ачение подаётся на линии микроадреса. Каждый бит адреса имеет либо подтягивающий транзистор к +5V, либо подтягивающий к земле транзистор. Такой подход несколько неэффективен, поскольку требует двух транзисторных позиций на каждый бит. Для сравнения, ПЗУ адресов переходов и ПЗУ адресов инструкций используют по одной транзисторной позиции на бит. (Как и в PLA, каждый транзистор либо присутствует, либо отсутствует по необходимости, поэтому фактическое число транзисторов меньше числа потенциальных позиций)

Поскольку конденсаторы довольно редко встречаются в NMOS-схемах, они показаны на изображении выше. Если линия поликремния пересекает легированный кремний, образуется транзистор. Однако если область поликремния просто располагается поверх легированного кремния без пересечения, формируется конденсатор. (Ёмкость присутствует и у транзистора, но ёмкость затвора обычно считается нежелательной)
14. Документация даёт косвенную подсказку о сложности микрокода загрузки констант. В частности, указано, что разные константы загружаются за разное количество тактов. Например, log₂(e) загружается за 18 тактов, log₂(10) — за 19, а log₁₀(2) — за 21. Можно было бы ожидать, что заранее вычисленные константы загружаются за одинаковое время, однако различия во времени показывают, что внутри происходит больше операций, чем кажется на первый взгляд.
Другие выпуски от Ken Shirrif
Невидимая оборона 386: как защищены входы и выходы процессора
Как сделать реверс-инжиниринг аналоговой микросхемы: FM-радиоприёмник TDA7000
Схемотехника стека сопроцессора Intel 8087 для чисел с плавающей запятой: реверс-инжиниринг
Два бита на транзистор: ПЗУ микрокода повышенной плотности в FPU-сопроцессоре Intel 8087
Анализ кристалла 8087: быстрый битовый шифтер математического сопроцессора
Внутри Intel 1405: фото кристалла памяти на регистре сдвига (1970)

Если вы дочитали до этого места, значит, уровень «использовать готовое» уже пройден, и становится важным понимание того, как всё устроено на уровне сигналов, логики и схем. Без этого сложно двигаться дальше — встраиваемые системы, железо и даже оптимизация кода начинают упираться в физику.
На курсе «Электроника и электротехника» как раз разбирают этот слой: от базовых принципов до проектирования собственных устройств и подготовки их к производству — с практикой в симуляторах и на реальных компонентах. Это способ перейти от чтения чужих решений к созданию своих.
Чтобы узнать больше о формате обучения и познакомиться с преподавателями, приходите на бесплатные уроки:
31 марта в 20:00. «Микроконтроллер на батарейках: аккумуляторы, стабилизаторы, зарядки и сон». Записаться
15 апреля в 20:00. «Автоматическая регулировка усиления в трактах АЦП: от микровольт до перегрузки без потери битов». Записаться
