§ Изменение концепции

Ранее я говорил о том, что делаю процессор 8088, основываясь на 8-битной шине данных. Так вот, посмотрел на то, сколько тратится тактов на каждую инструкцию и сильно, очень сильно разочаровался в таком подходе. Мне бы хотелось сделать процессор следующим образом:
  • Шина 32-х битная
  • Инструкции могут быть не только 8086, но и поддерживать 80186
  • Инструкция будет загружаться в "очередь инструкции" каждый раз, считывая до 6 байт
  • Исполнение инструкции не обязательно будет находится в posedge clock, но часть его вынесена в always @*-секцию
  • Некоторые простые инструкции теперь могут выполняться за 1Т
  • Байт modrm будет декодироваться полностью за 1Т
Но я не буду делать это, просто хочу немного отойти от темы и посмотреть, как можно декодировать инструкции еще.

§ Определение длины инструкции

У каждой инструкции есть своя длина. Чтобы определить длину инструкции, достаточно лишь знать ее опкод и, если есть, байт modrm. В таблице ниже я приведу карту базового набора инструкции.
000102030405060708090A0B0C0D0E0F
001212
101212
201212
301212
40
50
602211
701111111111111111
801211
904
A0222212
B01111111122222222
C011212321
D011
E0111111112241
F01*2*

Там, где опкод помечен зеленым, это значит, что за ним идет байт modrm, а значит, что инструкция как минимум содержит в себе два байта, но может и больше, потому что все определяется байтом modrm - там может быть считывание 1-байтного или 2-байтного смещения. В ячейках есть цифры 1 или 2, которые означают размер непосредственного значения (immediate), идущего за опкодом и байтом modrm, если есть. В случае когда нет ни байта modrm, ни immediate, то такой опкод является одиночным и занимает 1 байт.
Для этой цели я создам модуль в файле cpu_isize.v:
// verilator lint_off WIDTH
// verilator lint_off CASEX
// verilator lint_on  CASEINCOMPLETE

module cpu_isize
(
    input       [15:0]  op_queue,
    output reg          op_modrm_byte,
    output reg          op_modrm_mem,
    output reg  [1:0]   op_modrm_disp,
    output reg  [2:0]   op_imm_size
);

endmodule
Сверху закомментированы указания верилятору о том, что не надо предлагать casez и не проверять несоответствия ширины разрядности.
В качестве входного "аргумента" предлагается op_queue (48 бит или 6 байт), состоящего из опкода и возможного байта modrm, то есть, два байта. Опкод декодируется однозначно, а байт modrm просматривается только тогда, когда он в опкоде есть.
На выходе бит op_modrm_byte сигнализирует о том, что опкод имеет байт modrm, и потому надо будет проверять то, что на выходе op_modrm_disp, там может быть значение от 0 до 2. Независимо от modrm-байта, для каждого опкода также генерируется значение op_imm_size от 0 до 4 — количество байт непосредственного значения. Непосредственным значением также может быть 4-байтный переход по длинному адресу инструкции jmp far segment:offset или call far segment:offset.
Выход op_modrm_mem если 1, то значит, что в качестве операнда используется чтение из памяти.
Код, который расшифровывает наличие байта modrm, достаточно простой:
// Определить наличие байта modrm
always @* begin

    // Наличие байта ModRM
    casex (op_queue[7:0])
    8'b00xx_x0xx, // ALU mrm
    8'b0110_001x, // BOUND, ARPL
    8'b0110_10x1, // IMUL
    8'b1000_xxxx, // GrpArith,TEST,MOV,LEA,POP
    8'b1100_000x, // GrpShift
    8'b1100_01xx, // GrpShift
    8'b1101_00xx, // LES,LDS,MOV
    8'b1101_1xxx, // ESC, GrpMisc
    8'b1111_x11x: op_modrm_byte = 1'b1;
    default:      op_modrm_byte = 1'b0;
    endcase

end
В самом деле, это весь набор кодов для того, чтобы расшифровать, есть ли байт modrm или нет у опкода, но, опкод должен быть только из базового набора, без 0F-префикса.
Как ранее и говорилось, если у опкода есть байт modrm, то надо проверить, есть ли у него смещения при получении адреса памяти, где будет располагаться операнд.
op_modrm_disp   = 2'b00;
op_modrm_mem    = op_modrm_byte && op_queue[15:8] != 3'b11;

// Размер disp [0..2]
if (op_modrm_byte) begin

    casex (op_queue[15:8])
    8'b00_xxx_110,
    8'b10_xxx_xxx: op_modrm_disp = 2'h2;
    8'b01_xxx_xxx: op_modrm_disp = 2'h1;
    default:       op_modrm_disp = 2'b0;
    endcase

end
Сканирование дополнительных 2-х байт может быть только в случае если 1) используется offset16, 2) есть 16-битное слагаемое в вычислении эффективного адреса. Для считывания однобайтного знакового смещения есть mod=01, ну и для всех остальных будет 0, нет дополнительного смещения.
А теперь будет приведен код для самой значительной части, расчет количества байт, необходимых для непосредственного операнда:
casex (op_queue[7:0])
    // Однобайтные
    8'b00xx_x100, 8'b0110_101x, 8'b0111_xxxx, 8'b1000_00x0,
    8'b1000_0011, 8'b1010_1000, 8'b1011_0xxx, 8'b1100_000x,
    8'b1100_0110, 8'b1100_1101, 8'b1101_010x, 8'b1110_0xxx,
    8'b1110_1011:
        op_imm_size = 2'h1;
    // Двухбайтные
    8'b00xx_x101, 8'b0110_100x, 8'b1000_0001, 8'b1010_00xx,
    8'b1010_1001, 8'b1011_1xxx, 8'b1100_0010, 8'b1100_0111,
    8'b1100_1010, 8'b1110_100x:
        op_imm_size = 2'h2;
    // ENTER i16,i8
    8'b1100_1000:
        op_imm_size = 2'h3;
    // JMP|CALL far
    8'b1001_1010, 8'b1110_1010:
        op_imm_size = 3'h4;
    // TEST rm, i8/i16
    8'b1111_011x:
        if (op_queue[13:12] == 2'b00)
            op_imm_size = op_queue[0] + 1'b1;
    default:
        op_imm_size = 1'b0;
endcase
Вначале здесь выбираются такие маски опкодов, которые отвечают за 1,2,3,4-байтные случаи, но в конце видно, что есть специальные опкоды F6,F7, у которых имеется номер функции в байте modrm. Если этот номер функции 000 или 001, то это — инструкция TEST с непосредственным операндом. Для F6 это будет один байт, для F7 - два байта. Во всех остальных случаях, кроме описанных здесь, непосредственное значение не используется.
В данный момент материал находится в стадии разработки, поэтому пока что на этом все.
Я заканчиваю эту часть и начинаю следующую. У меня ранее был разработан процессор, который работает, используя 32-х битные операнды, так что теперь я просто буду объяснять то, как он работает.

4 мая, 2022
© 2007-2023 Мутил лиственный юг