Лисья Нора

Оглавление


§ Описание флагов

Что такое компьютер? Само по себе слово "компьютер" произошло от слова "compute" – вычислять, а значит, основная задача компьютера является вычисление, и, на самом деле, так оно и есть. Основная нагрузка происходит как раз на АЛУ – арифметико-логическое устройство.
Любые, даже самые сложные, операции, вроде нахождения косинуса или арктангенса, можно разбить на серию более мелких, сведя вообще все операции вычисления к сложению. Да, это слишком примитивно, но вместе с тем, основными элементарными операциями в x86 являются как раз 8 функции:
Три из восьми – это логические инструкции, пять оставшихся – арифметика. Я упомянул о флагах. В процессорах, которые используют флаги, каждый флаг что-то обозначает. К процессоре x86 флагов очень много, но в конкретном разрабатываемом мной процессоре их будет всего лишь 9:
11 10 9 8 | 7 6 5 4 3 2 1 0
O D I T | S Z - A - P - C
В регистрах 5 и 3 находятся нули, вероятно, в силу исторических причин, поскольку флаги расположены почти что идентично тем, что в z80 (хотя я в этом не уверен, что из-за этого), а в бите 1 регистра флага всегда установлена 1.
Что означают каждый флаг?

§ Разработка АЛУ

Приступаем к созданию кода на верилоге. Считаю, что будет нелишним сначала объявить именованные константы для положения флагов в регистре флагов:
localparam
CF = 0, PF = 2, AF = 4, ZF = 6, SF = 7,
TF = 8, IF = 9, DF = 10, OF = 11;
Например, CF находится в позиции 0, OF – в 11. В этой статье константы не понадобятся, но пускай будут объявлены. А теперь нужно объявить константы номеров функции АЛУ:
localparam
alu_add = 3'h0, alu_or = 3'h1,
alu_adc = 3'h2, alu_sbb = 3'h3,
alu_and = 3'h4, alu_sub = 3'h5,
alu_xor = 3'h6, alu_cmp = 3'h7;
Именно в таком порядке и никаком ином находятся номера функции АЛУ. Это касается не только положения в опкодах, но и специальных случаях с участием байта modrm.
Объявления сделаны, а теперь и сами вычисления:
wire [16:0] alu_r =
 
alu == alu_add ? op1 + op2 :
alu == alu_or ? op1 | op2 :
alu == alu_adc ? op1 + op2 + flags[CF] :
alu == alu_sbb ? op1 - op2 - flags[CF] :
alu == alu_and ? op1 & op2:
alu == alu_xor ? op1 ^ op2:
op1 - op2; // sub, cmp
Здесь реализован мультиплексор, на вход которого подаются различные операции (16 битные вычисления), и в качестве провода выбора выступает 3-х битный регистр alu. Поскольку sub и cmp выполняют одинаковую функцию, здесь они объединены.
То что alu_r не является 16-битным, а 17-битным, важно, потому что именно в 17 бит и находится тот самый флаг CF при сложении и вычитании. Создадим провод alu_top, он будет содержать старший бит, который зависит от текущего size, потому что флаги будут меняться в зависимости от того, какая в данный момент битность операндов, 8 или 16 бит.
wire [ 3:0] alu_top = size ? 4'hF : 3'h7;
Важным также является выбор группы для некоторых функции:
wire is_add = alu == alu_add || alu == alu_adc;
wire is_lgc = alu == alu_xor || alu == alu_and || alu == alu_or;
Если is_add=1, то текущая функция АЛУ – сложение, соответственно, тогда is_lgc=0. Если же is_add=0 и is_lgc = 1, это значит, что в данный момент выбрана группа логических операциях, ну и при is_add=0 и is_lgc=0 – группа вычитаний.
Рассмотрим теперь вычисления флага CF:
wire alu_cf = alu_r[alu_top + 1'b1];
Флаг будет равен либо 9-му биту alu_r[8], либо 17 alu_r[16], для этого как раз и используется провод alu_top.
wire alu_af = op1[4] ^ op2[4] ^ alu_r[4];
Почему флаг AF равен XOR между 4-ми битами первого, второго операнда и результат, можно [[/cpu/flag_half узнать]] из этой статьи.
wire alu_sf = alu_r[alu_top];
Флаг SF устанавливается тривиально, либо копируя бит alu_r[7], либо alu_r[15] – то есть, знаковый бит.
wire alu_zf = size ? ~|alu_r[15:0] : ~|alu_r[7:0];
Вот тут интересная хитрость. Когда size=1, то производится побитовое "ИЛИ" над всеми младшими 16 битами результата. Если там 0, то с помощью ~ это становится 1, сигнализируя о наличии нуля, и если хоть одна единица появится в одном из битов, то статус разрушается и zf превращается в 0. Аналогично с 8-битным результатом при size=0.
wire alu_pf = ~^alu_r[7:0];
С четностью так же как и с zero, но только примеряется не "OR", а "XOR". Эта операция проводит пере-xor-ивание между всеми битами, и при четном количестве единиц, получает 0, соответственно, с помощью инверсии ~ переворачивая в 1, и наоборот.
Пришло время для самого сложного вычисления overflow:
wire alu_of = (op1[alu_top] ^ op2[alu_top] ^ is_add) & (op1[alu_top] ^ alu_r[alu_top]);
Знающие люди скажут, что ничего сложного нет и что логическая его схема проста, но смотря на эту конструкцию, сомневаешься.
Существуют 2 случая, если is_add = 0 (вычитание) и is_add = 1 (сложение). Алгоритм вычисления этого флага меняется в зависимости от того, после какой инструкции был он рассчитан. Если после ADD, ADC, то результат op1[alu_top] ^ op2[alu_top] инвертируется, если после SUB,SBB,CMP, то нет.
И теперь, зная все флаги, можно сформировать их в конечном итоге в такую конструкцию:
wire [11:0] alu_f = {
 
/* OF */ alu_of & ~is_lgc,
/* DIT */ flags[10:8],
/* SF */ alu_sf,
/* ZF */ alu_zf,
/* 5 */ 1'b0,
/* AF */ alu_af & ~is_lgc,
/* 3 */ 1'b0,
/* PF */ alu_pf,
/* 1 */ 1'b1,
/* CF */ alu_cf & ~is_lgc
};
Флаги DIT при АЛУ-операциях не затрагиваются и потому просто копируются, биты 1,3,5 ставится 1,0,0 соответственно, а PF, ZF и SF записывается из результата непосредственно решение, а вот с OF, AF и CF не все так просто. С ними так – если вычисляется именно арифметическая инструкция, то is_lgc равен 0, то есть, alu_cf/pf/of присваиваются результату. В случае, если выполняется логическая инструкция (is_lgc=1), то cf, af и of устанавливаются в 0.
Как ни удивительно, но на этом моменте с АЛУ пока что все.

§ Исполнение инструкции

В этом материале я рассмотрю алгоритм работы инструкции АЛУ из набора [[/cpu/x86_table опкодов 00-3F]]. Маски опкодов будут выбраны следующие:
Схему работы первого метода я рассмотрел в предыдущем материале. Представим, что к фазе exec процессор пришел с правильными значениями op1, op2, alu и size. Раз все правильно, то будет исполнена та схема, которую я описал выше, то есть, будут доступны результата alu_r и alu_f.
// <ALU> modrm
8'b00_xxx_0xx: begin
 
flags <= alu_f;
wb <= alu_r;
t <= alu == alu_cmp ? init : wback;
 
end
Итак, во флаги будет записано новое вычисленное значение alu_f (в любом случае), в регистр wb будет записан результат. Если номер функции АЛУ равен CMP, то инструкция завершает свою работу и выходит к фазе init, иначе, переходит к фазе wback – записи результата в левый операнд байта modrm.
Регистр wb объявляется так:
reg [15:0] wb = 16'h0000;
То есть, это некий 16-разрядный регистр, который нужен для записи обратно в память или регистр. После того, как были вычислены операнды из байта modrm, в левый операнд и будет записано новое полученное значение. Дополним новые фазы:
localparam
init = 1'b0, // Стадия подготовки инструкции
...
wback = 4'hA, // Запись [wb,size,dir,modrm]
wback2 = 4'hB,
wback3 = 4'hC;
Фаза wback отвечает за сохранение в регистр или в память (младший байт); wback2 записывает в память старший байт регистр wb; wback3 останавливает запись, выключает we и переходит либо к фазе init, либо указанной ранее фазе в регистре tnext:
reg [ 5:0] tnext = 1'b0;
reg [ 2:0] m = 1'b0;
Здесь m – регистр для обозначения номер строки внутри фазы exec, поскольку инструкции обычно у меня не выполняются за один такт. Значение tnext всегда сбрасывается в init:
init: begin
...
tnext <= init;
Приведу код фазы wback:
// Запись wb [size,dir,modrm]
wback: begin
 
// Запись в регистр (reg) или reg:r/m-часть
if (dir || modrm[7:6] == 2'b11) begin
 
case (dir ? modrm[5:3] : modrm[2:0])
 
3'b000: if (size) ax <= wb; else ax[ 7:0] <= wb[7:0];
3'b001: if (size) cx <= wb; else cx[ 7:0] <= wb[7:0];
3'b010: if (size) dx <= wb; else dx[ 7:0] <= wb[7:0];
3'b011: if (size) bx <= wb; else bx[ 7:0] <= wb[7:0];
3'b100: if (size) sp <= wb; else ax[15:8] <= wb[7:0];
3'b101: if (size) bp <= wb; else cx[15:8] <= wb[7:0];
3'b110: if (size) si <= wb; else dx[15:8] <= wb[7:0];
3'b111: if (size) di <= wb; else bx[15:8] <= wb[7:0];
 
endcase
 
t <= tnext;
 
end
// Если modrm указывает на память, записать первые 8 бит
else begin
 
t <= size ? wback2 : wback3;
o_data <= wb[7:0];
we <= 1'b1;
 
end
 
end
Здесь довольно много кода, но всего лишь 2 случая.
wback3: begin we <= 1'b0; t <= tnext; end
Иначе, при 16-битном операнде, переходит к wback2:
// Запись старшего байта
wback2: begin
 
ea <= ea + 1;
o_data <= wb[15:8];
t <= wback3;
 
end
Там же записывается старший байт wb, сдвигается ea+1 и после этой микро-операции переходит к wback3, который и завершает работу фазы записи в память.
Таким образом, мы сделали сразу аж 32 инструкции! 8 функции на 4 разных типов операндов. Но поскольку будет очень удобно доделать АЛУ в этой секции опкодов в диапазоне 00-3F, то я доделаю.
8'b00_xxx_10x: case (m)
 
// Чтение 8 bit
0: begin
 
m <= size ? 1 : 2;
op1 <= ax;
op2 <= i_data;
ip <= ip + 1;
 
end
 
// Чтение 16 bit
1: begin op2[15:8] <= i_data; ip <= ip + 1; m <= 2; end
 
// Сохранение в AL/AX
2: begin
 
t <= init;
flags <= alu_f;
 
if (alu != alu_cmp)
begin if (size) ax <= alu_r; else ax[7:0] <= alu_r[7:0]; end
 
end
 
endcase
Это АЛУ, но с той разницей, что левый операнд либо регистр AL, либо AX, а правый – непосредственный операнд, идущий следом за опкодом.
Инструкция разделена на 3 строки. Как мы видим, при попадании в m=0 (линия 0), происходит формирование операндов op1, op2, второй операнд равен следующему байту после опкода. В зависимости от size, переход происходит либо на 1-ю линию (size=1), либо на 2-ю (size=0).
Вот теперь на этом можно сказать, все.