Лисья Нора

Оглавление


§ Общий алгоритм

Это одна из самых сложных вещей, которая касается процессора. Именно здесь больше всего происходит путаницы и страданий при попытке сделать декодер, поскольку сообщество давно привыкло к такому процессору как RISC-V при реализации, а вот CISC кажется сложным. Но не сегодня. Мы же знаем, что делаем учебный процессор на основе самых простейших и базовых принципов реализации, так что сделать декодер будет легче простого. Как по мне, реализовать процессорный вычислительный конвейер в десятки раз сложнее!
У меня уже есть две статьи на сайте в папке "Проекты", где я делал декодер не только для 8086, но и даже для 386 процессора. В отличии от тех статей, в этот раз я хочу разобрать всё настолько детально, насколько это будет возможно.
Первое что могу сказать, что вся логика будет исполняться в t=MODRM, а номер линии будет записан в регистр m1:
Строка #0 – основной такт
И все это делается за один такт.
Строка #1 – считывание disp8
Строка #2, #3 – считывание disp16
Строка #4, #5 – считывание операнда из памяти
Как видим, при задании 16-битных операндов и 16-битном смещении выполнение процедуры может занять до 6 тактов процессора. Это еще без учета такта, который считывает операнд, а также тактов, которые будут потрачены на запись результатов в память или регистр. Так что скорость исполнения инструкции может быть невысокой, если инструкция достаточно сложная.

§ Управляющие регистры

Перед тем как попасть в стадию считывания операндов, вначале необходимо прочитать префиксы и код операции. Префикс, в некотором роде считается кодом операции, но с тем отличием что при его исполнении он лишь только устанавливает значения в некоторые внутренние регистры, не завершая инструкцию в целом.
Когда инструкция завершается, то на последнем ее такте сбрасываются все управляющие потоком регистры, такие как m и префиксы для того чтобы новая инструкция не "тащила" за собой ранее установленный префикс от предыдущей.
Сделаем объявления всех управляющих регистров и разберем каждый из них подробно.
reg cp; // =1 Указатель на SGN:EA =0 Иначе CS:IP
reg cpm; // =0 Устанавливается cp после MODRM
reg size; // =1 16bit =0 8bit
reg dir; // =0 rm,r; =1 r,rm
reg cpen; // =0 То пропускает чтение операндов
reg over; // =1 Сегмент переопределен
reg intrc; // Предыдущее значение intr
reg [ 1:0] rep; // Наличие REP:
reg [ 3:0] t, next; // Исполняемая команда (t) в данный момент
reg [ 3:0] m, m1; // Фаза исполнения T (m), MODRM и субфаза
reg [ 2:0] m2, m3, m4; // Фаза исполнения WB, PUSH/POP
reg [ 2:0] alu; // Функция АЛУ или сдвигов
reg [ 7:0] modrm, opcache; // Кеш modrm и opcache
reg [ 7:0] interrupt; // Номер прерывания
reg [15:0] sgn, ea; // Выбранный SEGMENT:EA
reg [15:0] op1, op2, wb, t16; // Операнды; wb-что записывать
В этой статье я разместил все необходимые управляющие регистры для всего процессора в целом и на будущее, так что некоторые регистры тут будут пока не нужны, но потребуются в дальнейшем.
Как видно, количество временных регистров достаточно обширное, но не так велико, как это можно было себе представить. Большинство их них будут использоваться для чтения операндов. Для начала стоит исправить получение адреса в памяти:
assign a = cp ? sgn*16 + ea : cs*16 + ip;
И теперь информация будет читаться либо из памяти программ (cp=0) либо из памяти данных (cp=1). Но память данных и программ находятся в одном адресном пространстве, так что здесь это можно говорить скорее о выборе источника данных как такового. Для того чтобы читать операнды, требуется эффективный адрес, а использовать связку cs:ip мы можем только для чтения инструкции.

§ Чтение префиксов и опкода

До того как процессор дойдет до разбора и чтения операндов, он должен будет как минимум определить код операции. В предыдущей статье есть лишь минимальные сведения о том, как записать код операции в opcache, но сейчас я его дополню.
Скажем так, все действия выполняются в конструкции if (ce) begin /* вот тут */ end. Поскольку начало инструкции, которое ранее обозначили на проводе m0 теперь может быть не только началом реальной инструкции, но еще и префиксированной инструкции, то следует ввести еще один провод:
wire m0 = (t == RUN && m == 0);
wire c0 = m0 && {over, rep} == 3'b0;
Это означает следующее:
В соответствии с заведенными новыми регистрами первый такт для считывания новой инструкции или префикса теперь будет дополнен.
if (m0) begin
 
opcache <= i; // Записать опкод в кеш
next <= RUN; // По умолчанию возврат на RUN
ip <= ip + 1; // Инкремент программного счетчика
cpm <= 1; // По умолчанию ставить cp=1 после modrm
cpen <= 1; // Читать операнды из памяти из modrm
dir <= i[1]; // Направление операндов по умолчанию
size <= i[0]; // Битность 8/16 по умолчанию
m <= 1; // К следующей "строке" процедуры RUN
m1 <= 0; // Очистка m1, m2, m3, m4
m2 <= 0;
m3 <= 0;
m4 <= 0;
 
if (c0) sgn <= ds; // В начале инструкции sgn по умолчанию равен DS
 
end
Можно отметить то, что в начале инструкции мы видим что ни over ни rep не очищаются, поскольку это должно происходить исключительно только в конце исполнения каждой инструкции, иначе бы каждый раз терялись установленные префиксы, считанные до опкода.
Обозначим одну ветвь RUN для исполнения микро-операции.
case (t)
 
// ===============
RUN: casex (opcode)
 
// Выполнение опкодов или группы
[маска опкода]: case (m)
0: begin ... end
1: begin ... end
...
endcase
 
endcase
// ===============
 
endcase
Данная конструкция означает следующее:
Далее при детальной реализации каждой инструкции я буду расписывать их примерно в таком виде:
8'b001xx110: case (m) // ### Префикс ES/CS/SS/DS [1T]
 
0: begin
 
m <= 0; // Продолжим считывать префиксы или опкод
over <= 1; // Установим отметку, что был сегментный префикс
 
// Выбираем новый сегмент
case (opcode[4:3])
0: sgn <= es;
1: sgn <= cs;
2: sgn <= ss;
3: sgn <= ds;
endcase
 
end
 
endcase
Сама по себе инструкция расписана очень детально, из пояснений разве что могу сказать что в отличии от других реализации, в опкодах, которые означают префикс, не нужно завершать инструкцию, достаточно лишь указать что m=0. Для светлого будущего добавлю на всякий случай также обработку префикса REPNZ/REPZ:
8'b1111001x: case (m) // ### REPNZ, REPZ [1T]
 
0: begin m <= 0; rep <= opcode[1:0]; end
 
endcase
При указании префикса записывается 2 бита в регистр rep. Старший бит всегда равен 1, поскольку opcode[1]=1 по условию, а младший показывает тип префикса 0=REPNZ, 1=REPZ. Декодирования операндов это никак не касается, поскольку префикс REPx управляет строковыми инструкциями, но на всякий случай он был добавлен в этой главе.

§ Считывание операндов-регистров

Рассмотрим самый первый такт инструкции АЛУ для примера.
8'b00xxx0xx: case (m) // ### ALU-операции с операндами ModRM [3T+]
 
0: begin t <= MODRM; alu <= opcode[5:3]; end
 
endcase
На первом же такте из процедуры RUN передается управление процедуре MODRM, одновременно записывая в регистр alu номер функции АЛУ, которая будет впоследствии использована (в следующей главе подробнее об этом). Не стоит забывать то что ранее на том же такте #0 будет установлено значение m=1, так что когда процедура MODRM обратно передаст управление к RUN (возврат), то выполнение инструкции продолжится на такте #1 – то есть, выполнение будет именно продолжено. Это аналогично вызову процедуры из основной программы.
Кстати, в квадратных скобках буду указывать либо точное количество тактов, которые требуются на инструкцию, либо минимальное количество тактов, которое необходимо для исполнения инструкции. Например, инструкция АЛУ, описанная выше, может быть выполнена не менее чем за 3Т:
В случае если нужно будет записать результат в регистр, то потребуется еще 1 такт для этой цели – вызов процедуры WB.
Если мы получим байт modrm на вход, то гарантированно получим из него номер REG-части (биты 5..3), откуда необходимо будет извлечь значение указанного регистра. Однако, не стоит забывать о том моменте, что помимо REG-части еще есть и MEM-часть (биты 2..0), которая также может выступать как регистр, если задан MOD=11, так что придется подготовить два очень больших мультиплексора i20 и i53.
I. Мультиплексор на выбор регистров из диапазона [2:0]:
wire [15:0] i20 =
i[2:0] == 3'h0 ? (size ? ax : ax[ 7:0]) :
i[2:0] == 3'h1 ? (size ? cx : cx[ 7:0]) :
i[2:0] == 3'h2 ? (size ? dx : dx[ 7:0]) :
i[2:0] == 3'h3 ? (size ? bx : bx[ 7:0]) :
i[2:0] == 3'h4 ? (size ? sp : ax[15:8]) :
i[2:0] == 3'h5 ? (size ? bp : cx[15:8]) :
i[2:0] == 3'h6 ? (size ? si : dx[15:8]) :
(size ? di : bx[15:8]);
II. Мультиплексор на выбор регистров из диапазона [5:3]
wire [15:0] i53 =
i[5:3] == 3'h0 ? (size ? ax : ax[ 7:0]) :
i[5:3] == 3'h1 ? (size ? cx : cx[ 7:0]) :
i[5:3] == 3'h2 ? (size ? dx : dx[ 7:0]) :
i[5:3] == 3'h3 ? (size ? bx : bx[ 7:0]) :
i[5:3] == 3'h4 ? (size ? sp : ax[15:8]) :
i[5:3] == 3'h5 ? (size ? bp : cx[15:8]) :
i[5:3] == 3'h6 ? (size ? si : dx[15:8]) :
(size ? di : bx[15:8]);
Данные два мультиплексора в схеме занимают немалое количество логических элементов, учитывая также еще тот факт, что выбирать приходится не только регистры на 16 бит, но и 8 регистров по 8 бит, основываясь на указанной ранее размерности операнда size. Хотя если подумать о том, какие огромные мультиплексоры предстоит реализовать в процедуре RUN, то эти два уже не кажутся такими огромными.
С учетом уже выше сказанного, создаем новую процедуру:
MODRM: case (m1)
 
0: begin
 
modrm <= i;
ip <= ip + 1;
op1 <= dir ? i53 : i20;
op2 <= dir ? i20 : i53;
 
// Здесь вычислим эффективный адрес
// Определим, куда перейти дальше
// Выбираем sgn=ss если необходимо
 
end
 
endcase
Пока что это лишь заготовка кода для 0-й строки процедуры MODRM, в которой происходит следующее:
Тем самым, если dir=0, то у реализуется порядок следования операндов reg/mem, reg, и при dir=1, наоборот, будет reg, reg/mem.
По сути, если бы нам не надо было бы читать операнд из памяти, если он там не указан, то этого было бы достаточно для того чтобы вернуться к исполнению процедуры RUN и так оно и будет при условии MOD=11. Но так бывает далеко не всегда, так что придется вычислить и эффективный адрес, и смещение, если оно нужно, и прочесть операнд из памяти.

§ Считывание эффективного адреса

Зачастую, операнд бывает в памяти, а не в регистрах. Для того чтобы его извлечь, вначале требуется вычислить эффективный адрес, который идет вкупе с сегментными регистрами (по умолчанию DS). Для его расчета в 16-битном режиме используется определенная заранее заданная таблица, но я приведу вычисление этого адреса теперь в виде кода.
case (i[2:0])
3'b000: ea <= bx + si;
3'b001: ea <= bx + di;
3'b010: ea <= bp + si;
3'b011: ea <= bp + di;
3'b100: ea <= si;
3'b101: ea <= di;
3'b110: ea <= i[7:6] ? bp : 0;
3'b111: ea <= bx;
endcase
Этот код в целом достаточно просто понять, за исключением разве что только случая 3'b110. Дело в том что i[7:6] это и есть MOD часть, а i[2:0] отвечает за MEM часть байта modrm, и ранее было сказано что если mod=00 и mem=110, то вместо эффективного адреса, заданного косвенно регистрами или суммой регистров берется 16-битный адрес в памяти.
Но почему тут тогда в ea записывается 0? Это связано с тем, что когда процессор берет 16-битный адрес, то в данной ситуации, он еще дочитает 2 байта смещения в обычном режиме, как бы если был задан mod=10. В такой ситуации будет принудительно добавлено 16-битное смещение.
Теперь мы как раз и приходим к необходимости решить все случаи, что делать в том или ином значении mod и байта modrm, потому добавляется еще код, который занимается этой задачей.
casex (i)
8'b00_xxx_110: begin m1 <= 2; end // OFFSET16
8'b00_xxx_xxx: begin m1 <= 4; `CPEN; end // Читать операнд
8'b01_xxx_xxx: begin m1 <= 1; end // +Смещение 8 бит
8'b10_xxx_xxx: begin m1 <= 2; end // +Смещение 16 бит
8'b11_xxx_xxx: begin m1 <= 0; t <= RUN; end // Регистры. Вернуться к RUN
endcase
В коде использован макрос CPEN:
`define CPEN cp <= cpen; if (!cpen) begin m1 <= 0; t <= RUN; end
Согласно этому макросу, если ранее где-то указано cpen=0, то есть, не читать операнды и тогда после вычисления эффективного адреса и сегмента процессор вернется из процедуры назад в RUN. В данной ситуации эффективный адрес вычисляется без необходимости дочитать какое-либо смещение (ни 8, ни 16 битное).
И последний момент, который завершает логику работы #0 такта, будет выбор сегмента ss: в тех случаях, когда используется регистр bp в вычислении эффективного адреса.
if (!over && ((^i[7:6] && i[2:0] == 3'b110) || i[2:1] == 2'b01)) sgn <= ss;
Ситуации может быть несколько:
Но если есть over=1, то сегмент SS не выбирается, поскольку префикс имеет больший приоритет.
Итак, когда был завершен первый такт, то, в зависимости от разного типа операндов и эффективного адреса, может быть направлен ход исполнения инструкции в разные процедуры – либо продолжен считывания, в том числе, смещения к эффективному адресу, либо сразу выход к RUN, либо переход к чтению операндов.
Рассмотрим чтение смещений в процедуре MODRM:
1: begin m1 <= 4; ip <= ip + 1; ea <= ea + sign; `CPEN; end
2: begin m1 <= 3; ip <= ip + 1; ea <= ea + {8'h00, i}; end
3: begin m1 <= 4; ip <= ip + 1; ea <= ea + {i, 8'h00}; `CPEN; end
Здесь добавился еще один провод sign, который расширяет знак входящего числа с шины данных с 8 до 16 бит путем копирования бита 7 в старшие биты 8..15:
wire [15:0] sign = {{8{i[7]}}, i};

§ Считывание операндов

Наконец, после того как был прочитан эффективный адрес, указатель в памяти на него установлен, пришло время прочитать уже сами операнды из памяти. Когда процессор приходит в точку m1=4, то он уже должен переключить cp=1, а sgn:ea должны быть настроены на правильно вычисленный адрес для того чтобы как минимум прочитать 8-битное число либо во временный регистр op1 (операнд-назначение, слева) либо в op2 (операнд-источник, справа).
Чтение операнда из памяти не такая сложная задача. Рассмотрим получение младшего байта.
4: begin
 
if (dir) op2 <= i; else op1 <= i;
if (size) begin m1 <= 5; ea <= ea + 1; end
else begin m1 <= 0; cp <= cpm; t <= RUN; end
 
end
5: begin
 
if (dir) op2[15:8] <= i; else op1[15:8] <= i;
 
t <= RUN;
m1 <= 0;
ea <= ea - 1;
cp <= cpm;
 
end
Все аналогично предыдущей строке исполнения, но отличие в том, что после выхода из процедуры указатель эффективного адреса ea обратно устанавливается на верную позицию, поскольку это будет необходимо при записи результата вычислений обратно в память, но об этом уже в следующей главе.
Конечно, нельзя сказать что разработка декодера операндов – простая задача, но и не настолько сложная, если понимать принципы работы базового, 16 битного декодера, хотя и в 32-битном тоже вполне можно разобраться.

§ Диаграмма исполнения

Дана инструкция: 36 01 06 06 00 и данные 90 EF BE, которые идут после нее. Диаграмма выполнения инструкции будет следующей:
CLOCK_100CLOCKRST_NxM0x361OPCODExDIRxSIZExOVERx010Tx001Mx023450M1xF000CSxFFFFSSxCPxFFF0FFF1FFF2FFF3FFF4FFF5IPxFFFF0FFFF1FFFF2FFFF3FFFF4FFFF6FFFF7FFFF6Ax36160EFBEEFIxOxWxFFFFSGNx0676EAx0EFBEEFOP1x0OP2
На схеме после исполнения считывания операндов инструкция "обрывается" на полуслове (m=1,m1=0,t=RUN), потому что она не была завершена до конца нужным образом, но этот момент оставляем на будущие главы.
Скачать исходный код