Лисья Нора

Оглавление


§ Обратная запись

В предыдущей главе было подробно и максимально детально разобран процесс считывания префикса, опкода, эффективного адреса и операндов. В этой главе поговорим и реализуем процедуры, которые будут необходимы в дальнейшем исполнении инструкции. Эти процедуры, которые будут реализованы в данной главе, являются "сердцем" всей системы, поскольку сами по себе инструкции и их выполнение – задача уже рутинная.
Обычно полный цикл инструкции заключается в следующем:
В самом деле эти этапы вполне могут помещаться в вычислительный конвейер, если его начать делать, но, как все понимают, никто сейчас делать его точно не будет, по крайней мере, на данный момент. Возможно что в будущих главах или специальном дополнении.
Когда результат выполнения инструкции или же какой-то отдельной операции готов, то необходимо чтобы у нас была процедура, которая занимается тем, что записывает результаты. В качестве входящих данных у этой процедуры будут следующие регистры:
Так что список аргументов для процедуры под названием WB будет весьма немалым и за этим необходимо будет строго следить, чтобы не упустить необходимых деталей.

§ Доработка инструкции

Для начала давайте доработаем инструкцию, которую начали делать в прошлой главе.
8'b00xxx0xx: case (m) // ### ALU-операции с операндами ModRM [3T+]
 
0: begin t <= MODRM; alu <= opcode[5:3]; end
1: begin t <= alu == CMP ? RUN : WB; wb <= ar; flags <= af; `TERM end
 
endcase
Касательно первого такта, тут все понятно. Записывается номер функции АЛУ, которую необходимо выполнить и передается управление процедуре MODRM, которая считывает необходимые операнды в регистры op1, op2.
Второй такт инструкции делает следующее:
`define TERM {m, rep, over} <= 1'b0;
При помощи этого макроса нам не придется раз за разом писать обновления некоторых регистров в нулевое значение. Достаточно просто написать макрос, который является признаком окончания инструкции в том смысле, что он и очищает предыдущие префиксы, если они были, и переходит к нулевой строке инструкции (m=0).
Отмечу то что этот макрос не переводит t=RUN, потому что хоть инструкция и будет завершаться, но она необязательно переходит именно к стадии выполнения из-за той причины, когда мы вызываем процедуру WB, например, которая сначала запишет результаты в память или регистр и уже потом, по завершении, перейдет к считыванию новой инструкции.
Завершать инструкцию строго необходимо именно так, очисткой префиксов. Если бы этого не было, то ранее установленный префикс так бы и "тащился" от инструкции к инструкции, и это было бы ошибкой.
Если упомянули про номера функции к АЛУ, то следует их выписать в виде констант:
localparam ADD = 0, OR = 1, ADC = 2, SBB = 3, AND = 4, SUB = 5, XOR = 6, CMP = 7;
localparam ROL = 0, ROR = 1, RCL = 2, RCR = 3, SHL = 4, SHR = 5, SAL = 6, SAR = 7;
localparam CF = 0, PF = 2, AF = 4, ZF = 6, SF = 7, TF = 8, IF = 9, DF = 10, OF = 11;
Выпишу сразу всё, включая номера позиции флагов в регистре флагов. Например, флаг SF занимает 7-й бит в регистре флагов, а флаг OF – 11-й бит. Про то как работают остальные функции, например SBB или SHR – об этом будет написано в следующих главах, а пока мы знаем только их номера.
В качестве заглушки поставим на выход АЛУ строгие нули.
wire [15:0] ar = 1'b0;
wire [11:0] af = 1'b0;
Сегодня реализовывать работу АЛУ не будем, но для того чтобы запустить схему, необходимо, чтобы эти провода были где-то и имели определенное значение.

§ Процедура WB

Переходим, наконец, к реализации второй по значимости процедуры после считывания операндов, а именно, записи результата. Для начала определимся с порядком действий. Когда мы попадаем в данную процедуру, в качестве основного аргумента, конечно, будет значение в wb, которое может быть как 8 битным (size=0) так и 16-битным (size=1). Но как определить, куда записывать?
Это значит, что в зависимости от условия в dir и modrm возможно однозначно определить, писать ли результат в регистр, и какой именно по счёту. Реализуем первый такт процедуры WB.
case (t)
...
WB: case (m2)
 
// Записать в регистры, если это явно указано
0: if (dir || modrm[7:6] == 2'b11) begin
 
case (dir ? modrm[5:3] : modrm[2:0])
0: if (size) ax <= wb; else ax[ 7:0] <= wb[7:0];
1: if (size) cx <= wb; else cx[ 7:0] <= wb[7:0];
2: if (size) dx <= wb; else dx[ 7:0] <= wb[7:0];
3: if (size) bx <= wb; else bx[ 7:0] <= wb[7:0];
4: if (size) sp <= wb; else ax[15:8] <= wb[7:0];
5: if (size) bp <= wb; else cx[15:8] <= wb[7:0];
6: if (size) si <= wb; else dx[15:8] <= wb[7:0];
7: if (size) di <= wb; else bx[15:8] <= wb[7:0];
endcase
 
t <= next;
cp <= 0;
 
end
// Либо записать в память младший байт wb
else begin w <= 1; cp <= 1; m2 <= 1; o <= wb[7:0]; end
 
endcase // Завершение m2
...
endcase // Завершение t
Теперь разберем код. Как видим, было добавлена новая процедура WB, в которой в качестве номера строки выступает регистр m2, всегда начинающийся с 0.
В такте #0 данной процедуры есть ветвление:
Это то, что происходит именно на первом такте. А вот #1 такт сделан весьма необычным способом.
1: begin
 
w <= size;
cp <= size;
m2 <= size;
t <= size ? WB : next;
ea <= ea + 1;
o <= wb[15:8];
size <= 0;
 
end
Здесь могут быть 2 варианта.
Это значит, что когда size=1, то сначала дополнительно дописывается старший байт wb в память, а уже потом, на следующем такте, завершается запись и происходит возврат из процедуры. Вот такая вот хитроумная конструкция!
На этот раз инструкцию реализовать удалось в почти полном объеме. Единственная деталь, немаловажная конечно, это вычисление самого значения АЛУ. Сейчас запись в память или регистр будет производиться, но лишь только нуль.

§ Работа со стеком

Еще две важные процедуры, которые рассмотрим. Стек – это некоторый указатель в память по адресу ss:sp, в котором по методу LIFO складываются временные значения – аргументы процедур, обратные адреса возврата, флаги и прочие данные. Отличие стека от других видов памяти в том что он работает, упрощенно говоря так: "первый зашел – последний вышел", или если представить себе груду тарелок: сначала мы кладем первую тарелку, и так накладываем их до верху, и когда начинаем извлекать тарелки, то та тарелка, которая была положена последней, будет взята именно первой. А первая положенная тарелка будет взята последней.
Этот принцип только с виду кажется нелогичным. На деле он совершенно необходим в работе программ. Когда программа вызывает очередную процедуру через инструкцию CALL, то она укладывает в стек значение адреса возврата. Стек увеличивается на +1 и если будет вызвана еще одна процедура внутри первой процедуры, то в стек укладывается еще одно значение адреса возврата и так далее. Когда придет время вернуться из процедуры, то тогда RET вернет значение ip не к тому значению первого вызова CALL, а именно к последнему вызову, и следующий RET – к предпоследнему и так до тех пор, пока не будет возвращен адрес первого вызова процедуры.
Необходимо реализовать две процедуры: PUSH – вставка нового значения в стек, и POP – извлечение. Вершина стека всегда находится по адресу ss:sp и сам стек всегда бывает либо 16-битным, либо 32-х битным. Когда он становится 32-х битным, зависит от размерности селектора ss в защищенном режиме работы процессора. То есть, при вызове CALL в стек добавляется 32-битное число только если селектор 32 битный. Но это тонкости, которые сейчас не важны на данном этапе.
Код для реализации PUSH, который "заталкивает" 16-битное значение регистра wb довольно прост:
PUSH: case (m3)
0: begin cp <= 1; sp <= sp - 2; sgn <= ss;
m3 <= 1; ea <= sp - 2; w <= 1; o <= wb[ 7:0]; end
1: begin m3 <= 2; ea <= ea + 1; w <= 1; o <= wb[15:8]; end
2: begin m3 <= 0; cp <= 0; t <= next; sgn <= ds; end
endcase
Сама по себе процедура состоит из 3 тактов. Разберу их подробнее.
Очень важно именно записать в sgn значение ds, поскольку после того как будет выполнена инструкция, в этом регистре будет значение сегмента ss, что может поломать работу некоторых, например, строковых инструкции или тех, которые зависят от сегмента sgn уже на первом такте. Таких инструкции немного, но они есть и это надо учитывать. Да и вообще это как минимум хороший тон, подчищать хвосты после очередной инструкции.
Реализация процедуры POP происходит примерно похожим образом. Только вместо записи wb, из памяти регистр этот читается.
POP: case (m3)
0: begin m3 <= 1; cp <= 1; ea <= sp; sp <= sp + 2; sgn <= ss; end
1: begin m3 <= 2; wb <= i; ea <= ea + 1; end
2: begin m3 <= 0; wb[15:8] <= i; cp <= 0; t <= next; sgn <= ds; end
endcase
И как и в прошлом случае, разберем по тактам.
Данные две процедуры, реализованные сегодня в этой главе очень активно участвуют в выполнении инструкции, которые как-то связаны с работой со стеком. Это и PUSH, POP, это и CALL, RET, INT и другие.

§ Вызов прерывания

Последняя процедура, которую разберем сегодня, это будет процедура прерывания. Она не последняя из общего списка, поскольку есть еще DIV (деление) и даже UNDEF, но процедура прерывания как раз использует вызов подпрограммы (PUSH).
Прерывания могут вызываться разными способами.
Сама по себе она выполняет следующие действия:
Теперь сам код процедуры.
INTERRUPT: case (m4)
0: begin m4 <= 1; t <= PUSH; wb <= {4'hF, flags}; next <= INTERRUPT; end
1: begin m4 <= 2; t <= PUSH; wb <= cs; end
2: begin m4 <= 3; t <= PUSH; wb <= ip; end
3: begin m4 <= 4; ea <= {interrupt, 2'b00}; sgn <= 0; cp <= 1; end
4: begin m4 <= 5; ip[ 7:0] <= i; ea <= ea + 1; end
5: begin m4 <= 6; ip[15:8] <= i; ea <= ea + 1; end
6: begin m4 <= 7; cs[ 7:0] <= i; ea <= ea + 1; end
7: begin m4 <= 0; cs[15:8] <= i; cp <= 0; t <= RUN; flags[IF] <= 1'b0; sgn <= ds; end
endcase
Это одна из самых длинных процедур. Как видно, делаются 3 вызова подпрограммы PUSH и на каждый из них тратится 3 такта (+1 такт самого вызова), и по итогу сама по себе процедура вызова прерывания составляет 17 тактов! Хотя для такой процедуры – это не настолько уж и много, если так подумать.
На последнем такте инструкции #7 как и обычно, происходит стандартная процедура возврата к началу выполнения следующей инструкции, по аналогии с ранее разобранными. Единственное отличие заключается только в том, что ставится флаг IF=0. Как я и писал выше, это нужно для предотвращения повторного вызова во время исполнения обработчика прерываний.

§ Временная диаграмма

В отличии от предыдущей главы, теперь можно увидеть полный цикл обработки одной достаточно длинной инструкции.
CLOCK_100CLOCKRST_NxM0x361090OPCODExDIRxSIZExOVERx01020Tx00101Mx023450M1xF000CSxFFFFSSxCPxFFF0FFF1FFF2FFF3FFF4FFF5FFF6IPxFFFF0FFFF1FFFF2FFFF3FFFF4FFFF6FFFF7FFFF6FFFF7FFFF5FFFF6Ax36160EFBEEF0BE090Ix0OxWxFFFF0SGNx067678EAx0EFBEEFOP1x0OP2
На графике видно, что в конце происходит перезапись результатов (wb=0000h) в область памяти. И хотя это пока что неправильная работа инструкции, поскольку она еще не до конца реализована сейчас, самое важное, что принцип работы здесь верный именно с точки зрения самой логики процедуры обратной записи.
Скачать исходный код