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

Что такое компьютер? Само по себе слово "компьютер" произошло от слова "compute" — вычислять, а значит, основная задача компьютера является вычисление, и, на самом деле, так оно и есть. Основная нагрузка происходит как раз на АЛУ — арифметико-логическое устройство.
Любые, даже самые сложные, операции, вроде нахождения косинуса или арктангенса, можно разбить на серию более мелких, сведя вообще все операции вычисления к сложению. Да, это слишком примитивно, но вместе с тем, основными элементарными операциями в x86 являются как раз 8 функции:
  • ADD, ADC - сложение двух операндов без переноса и с учетом переноса
  • SUB, SBB - вычитание одного из другого с переносом (sbb) и без переноса (sub)
  • CMP - аналогично SUB, но результат вычисления не сохраняется, только обновляются флаги
  • XOR - логическое битовое исключающее или
  • AND - логическое битовое "и"
  • OR - логическое битовое "или"
Три из восьми - это логические инструкции, пять оставшихся - арифметика. Я упомянул о флагах. В процессорах, которые используют флаги, каждый флаг что-то обозначает. К процессоре 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.
Что означают каждый флаг?
  • C — carry, флаг, который устанавливается в 1, когда результат предыдущей инструкции превысил максимальное значение. К примеру, если был добавлен 255+1 в 8-битном регистре, что дает результат 0, но флаг C=1 говорит о том, что только что был перенос
  • P — parity, флаг четности, который устанавливается в случае, если младшие 8 бит результата — четные, или количество единиц — четно, флаг этот весьма устаревший и почти никогда нигде не применяется и нужен лишь для обеспечения совместимости с возможным старым ПО. Говорят, что он был нужен для приема данных с внешнего мира, считать биты четности. Но это давно уже все устарело
  • A — aux, полуперенос из разряда 3 в разряд 4, аналогично предыдущему флагу P, он устарел, применяясь только для инструкции коррекции DAA, DAS, AAA, AAS. Применялся для двоично-десятичной логики, но сейчас это стало настолько неактуально, что сами инструкции, которые работают с этим флагом, вообще удалили из x86-64
  • Z — zero, ставится в 1, если результат выполнения АЛУ был нуль
  • S — sign, всегда копируется из старшего бита результата, 7-го, 15-го, 31-го или 63-го, в зависимости от того, с какой разрядностью была исполнена инструкция
  • T — trap, если установлен в 1, то после исполнения каждой инструкции вызывается int 1, нужен был раньше для отладчиков, сейчас уже устарел
  • I — interrupt, при установленном =1, разрешается вызов прерываний во время исполнения кода
  • D — direction, нужен для работы строковых инструкции, если D=1, то строка идет вниз, иначе вверх, то есть по инкременту
  • O — overflow, самый сложный флаг из всех, который ставится в 1, если произошло знаковое переполнение, к примеру, если вычесть из -128 единицу (в 8-битном числе), то должно получиться -129, но на самом деле получится +127. Поскольку получится неправильный результат со знаками, говорят, что произошло знаковое переполнение.

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

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

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

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