Некоторое время назад стало доступно описание на языке Verilog легендарного микропроцессора 8080, полученное реверс-инжинирингом с фотографии кристалла его советского клона — КР580ВМ80А. Кому интересно техническое изложение про авторскую реализацию этого процессора — добро пожаловать под кат. Устроен 8080 просто, занимает около тысячи строк на Verilog.

Предварительные замечания

Для начала, полезно изучить даташит процессора. Intel приводит подробное описание 8080, вплоть до разбивки инструкций по тактам машинных циклов.

Не обойтись также без знания концепций, на которых построен 8080, — например учебного процессора SAP-1 из книги Malvino «Digital Computer Electronics».

В процессоре используется двухфазный тактовый сигнал f1, f2. Однако в данном Verilog-коде добавлен искусственный тактовый сигнал clk более высокой частоты, элементы схемы переключаются по его фронту. Будем считать, что это вызвано особенностями современных FPGA-микросхем.

тактовый сигнал
тактовый сигнал

Чистый код

Некоторые короткие идентификаторы пришлось переименовать для лучшего понимания. Чтобы можно было параллельно читать оригинальный vm80a.v, вот таблица соответствия:

таблица соответствия

Оригинальное название

Переименованное

mxr0

sel_pc

mxr1

sel_de

mxr2

sel_hl

mxr3

sel_bc

mxr4

sel_sp

mxr5

sel_wz

mxo

r16_val

mxi

addr_plus

mxwadr

r16_val_TO_addr

mxw16

addr_plus_TO_r16

mxwl

data_TO_r16_low

mxwh

data_TO_r16_high

mxrl

r16_val_low_TO_data

mxrh

r16_val_high_TO_data

alu_xout

alu_xr_TO_x_inv

alu_xwr

alu_data_TO_xr

alu_xrd

alu_xr_TO_data

alu_ald

alu_s_TO_acc

alu_awr

alu_data_TO_acc

alu_ard

alu_acc_TO_data

alu_rld

alu_acc_TO_r

alu_r00

alu_minus1_TO_r

alu_rwr

alu_data_TO_r

alu_srd

alu_s_TO_data

alu_zrd

alu_00_TO_data

alu_frd

alu_psw_TO_data

psw_ld

data_TO_psw

psw_wr

res_TO_psw

Операционная часть процессора

Начнем с более простой части процессора.

Регистровый файл

Процессор содержит 16-битные регистры PC, SP и 8-битные регистры B, C, D, E, H, L, объединенные в пары. Регистр A (аккумулятор) относится к АЛУ и здесь его нет. Также есть регистры W, Z для внутренних нужд — программно они не доступны.

reg   [15:0] r16_pc, r16_hl, r16_de, r16_bc, r16_sp, r16_wz, r16_val;
wire  [15:0] addr_plus;

Операции: чтение и запись. Регистровая пара, над которой проводится операция, выбирается одним из сигналов sel_pc. 16-битное чтение и запись (на шину адреса a) осуществляется сигналами addr_plus_TO_r16, r16_val_TO_addr. 8-битное (на шину данных d) — сигналами data_TO_r16_high, data_TO_r16_low, r16_val_high_TO_data, r16_val_low_TO_data.

wire sel_pc, sel_de, sel_hl, sel_bc, sel_sp, sel_wz;
wire data_TO_r16_high, data_TO_r16_low, r16_val_high_TO_data, r16_val_low_TO_data, addr_plus_TO_r16, r16_val_TO_addr;

//выбор регистровой пары
always @ (*)
case ({sel_pc, sel_de, sel_hl, sel_bc, sel_sp, sel_wz})
   6'b100000:  r16_val = r16_pc;
   6'b010000:  r16_val = r16_hl;
   6'b001000:  r16_val = r16_de;
   6'b000100:  r16_val = r16_bc;
   6'b000010:  r16_val = r16_sp;
   6'b000001:  r16_val = r16_wz;
   default:    r16_val = 16'h0000;
endcase

always @ (posedge clk)
if (f2)
begin
   //16-битное чтение
   if (r16_val_TO_addr) a <= r16_val;
   //16-битная запись
   if (addr_plus_TO_r16)
      begin
         if (sel_pc) r16_pc <= addr_plus;
         if (sel_de) r16_hl <= addr_plus;
         if (sel_hl) r16_de <= addr_plus;
         if (sel_bc) r16_bc <= addr_plus;
         if (sel_sp) r16_sp <= addr_plus;
         if (sel_wz) r16_wz <= addr_plus;
      end
   else
      begin
         //8-биная запись
         if (data_TO_r16_low)
            begin
               if (sel_pc) r16_pc[7:0] <= d;
               if (sel_de) r16_hl[7:0] <= d;
               if (sel_hl) r16_de[7:0] <= d;
               if (sel_bc) r16_bc[7:0] <= d;
               if (sel_sp) r16_sp[7:0] <= d;
               if (sel_wz) r16_wz[7:0] <= d;
            end
         if (data_TO_r16_high)
            begin
               if (sel_pc) r16_pc[15:8] <= d;
               if (sel_de) r16_hl[15:8] <= d;
               if (sel_hl) r16_de[15:8] <= d;
               if (sel_bc) r16_bc[15:8] <= d;
               if (sel_sp) r16_sp[15:8] <= d;
               if (sel_wz) r16_wz[15:8] <= d;
            end
      end
end

//8-биное чтение
assign d    = //...
               | r16_val_low_TO_data & r16_val[7:0]
               | r16_val_high_TO_data & r16_val[15:8]
               | //...
               ;

//16-битная запись
assign addr_plus = inc16 ? (a + 16'h0001)
                  : dec16 ? (a - 16'h0001)
                  : a;

АЛУ

Арифметические и логические операции в процессоре 8080 являются одноадресным — одним из операндом всегда выступает аккумулятор (регистр A).

Содержимое аккумулятора передается АЛУ в аргументе r, второй аргумент — в x, результат операции содержится в s.

reg   [7:0]    xr, r, acc;
wire  [7:0]    x, s, c;
wire           cl, ch;
reg            psw_z, psw_s, psw_p, psw_c, psw_ac, tmp_c;

assign s  = {7'b0000000, id_rlc & c[7]}
          | ((id_rxc | id_ora | id_ana | id_xra) ? 8'h00 : (x + r + cl))
          | (id_rxc  ? {ch, r[7:1]} : 8'h00)
          | (id_ora  ? (x | r) : 8'h00)
          | (id_ana  ? (x & r) : 8'h00)
          | (id_xra  ? (x ^ r) : 8'h00);

assign cl = (id_daa | id_rlc | id_ora | id_xra | id_rxc) ? 0 : tmp_c;
assign ch = id_rar ? tmp_c | r[0];

assign c[0] = (r[0] & x[0]) | (cl & (r[0] | x[0]));
assign c[1] = (r[1] & x[1]) | (c[0] & (r[1] | x[1]));
assign c[2] = (r[2] & x[2]) | (c[1] & (r[2] | x[2]));
assign c[3] = (r[3] & x[3]) | (c[2] & (r[3] | x[3]));
assign c[4] = (r[4] & x[4]) | (c[3] & (r[4] | x[4]));
assign c[5] = (r[5] & x[5]) | (c[4] & (r[5] | x[5]));
assign c[6] = (r[6] & x[6]) | (c[5] & (r[6] | x[6]));
assign c[7] = (r[7] & x[7]) | (c[6] & (r[7] | x[7]));

При сложении и вычитании используется строка s = (x + r + cl).

Для логической операции используется соответствующая строка (x | r), (x & r), или (x ^ r).

Для сдвига влево используется строка s = (x + r + cl), при этом x и r содержат одинаковое значение.

Для сдвига вправо используется строка s = {ch, r[7:1]}.

Операции:

  • alu_xr_TO_x_inv — инвертировать второй аргумент,

  • alu_data_TO_xr — взять второй аргумент из шины данных,

  • alu_xr_TO_data — записать второй аргумент на шину данных,

  • alu_s_TO_acc — записать результат операции в аккумулятор,

  • alu_data_TO_acc — записать в аккумулятор содержимое шины данных,

  • alu_acc_TO_data — записать аккумулятор на шину данных,

  • alu_acc_TO_r — взять первый аргумент из аккумулятора,

  • alu_minus1_TO_r — заполнить первый аргумент значением -1,

  • alu_data_TO_r — взять первый аргумент из шины данных,

  • alu_s_TO_data — записать результат операции на шину данных,

  • alu_00_TO_data — записать 0 на шину данных,

  • alu_psw_TO_data — записать флаги (признак переноса и т.п.) на шину данных,

  • t2175 — заполнить входящий перенос tmp_c из признака переноса,

  • заполнить входящий перенос tmp_c подходящим для выполняемой инструкции значением.

wire alu_xr_TO_x_inv, alu_data_TO_xr, alu_xr_TO_data, alu_s_TO_acc, alu_data_TO_acc, alu_acc_TO_data,
     alu_acc_TO_r, alu_minus1_TO_r, alu_data_TO_r, alu_s_TO_data, alu_00_TO_data, alu_psw_TO_data;
reg  data_TO_psw, res_TO_psw;

//alu_xr_TO_x_inv
assign x = alu_xr_TO_x_inv ? ~xr : xr;

//alu_xr_TO_data
assign d    = ~alu_00_TO_data & (
               //...
               | alu_xr_TO_data & xr
               | alu_acc_TO_data & acc
               | alu_psw_TO_data & psw_s
               | alu_s_TO_data & s
               //...
               );

always @(posedge clk)
begin
   if (f1)
      begin
         t2133 <= ~id_rxc & c[7];
      end
   if (f2)
      begin
         if (alu_data_TO_xr) xr <= id_rst ? (i & 8'b00111000) : d;
         if (alu_data_TO_acc) acc <= d;
         if (alu_s_TO_acc) acc <= s;
         if (alu_acc_TO_r) r <= acc;
         if (alu_data_TO_r) r <= d;
         if (alu_minus1_TO_r) r <= 8'hff;
         if (data_TO_psw)
            begin
               psw_c  <= d[0]; 
               psw_p  <= d[2];
               psw_z  <= d[6];
               psw_s  <= d[7];
            end
         if (res_TO_psw)
            begin
               psw_p  <= ~(^s);
               psw_z  <= ~(|s);
               psw_s  <= s[7];
            end
         if (t2222)
            begin
               if (id_xra | id_stc | id_ora | id_ana | id_cmc)
                  psw_c <= ~tmp_c;
               if (id_cmp | id_sbb | id_sub)
                  psw_c <= ~(t2133 | id_rxc & x[0]);
               if (id_dad | id_sha | id_adc | id_add)
                  psw_c <= t2133 | id_rxc & x[0];
            end
         if (t2175)
            tmp_c <= psw_c;
         if (t4f1)
            begin
               if (id_sbb)
                  tmp_c <= ~psw_c;
               if (id_inr | id_ora | id_xra | id_ana | id_cmp | id_sub)
                  tmp_c <= 1'b1;
               if (id_dad | id_cma | id_dcr | id_add | id_stc)
                  tmp_c <= 1'b0;
            end
      end
end

Управляющая часть процессора

Эта самая непонятная часть кода, тем более что описание процессора получено реверс-инжинирингом. Поэтому самый верный способ изучить логику формирования управляющих сигналов — посмотреть на них в симуляторе отдельно для каждой инструкции.

Можно отметить, что инструкции объединены в классы, если есть что-то общее. Среди них есть классы с понятным смыслом: например, id_1cycle, id_4cycle, id_3cycle указывают на количество машинных циклов. Однако, смысл части классов не понятен. Они так и названы: id00, id01, и т.д. Есть подозрение, что авторы использовали подходящую программу для оптимизации, она и выделила эти классы.

Машинный цикл

Выполнение инструкции разбито на машинные циклы, а машинные циклы — на такты (этапы, в даташите именуются state). Процессор 8080 относится к процессорам с переменным числом машинных циклов и тактов. Максимально может быть 5 циклов: m1, m2, m3, m4, m5. И 6 тактов: t1, t2, tw, t3, t4, t5.

Во время выполнения машинного цикла осуществляется одна операция доступа к памяти (чтение или запись).

Переключением циклов управляет (кольцевой) счетчик машинных циклов:

always @(posedge clk)
begin
   if (f1)
      begin
         m1f1 <= m1;
         m2f1 <= m2;
         m3f1 <= m3;
         m4f1 <= m4;
         m5f1 <= m5;
      end
   if (f2)
      begin
         m1 <= (~ms0 & ~ms1) | (~ms1 & m1f1);
         m2 <= (~ms0 | ~ms1) & ((ms0 & m2f1) | (ms1 & m1f1));
         m3 <= (ms0 & m3f1) | (ms1 & m2f1);
         m4 <= (ms0 & m4f1) | (ms1 & m3f1) | (ms0 & ms1);
         m5 <= (ms0 & m5f1) | (ms1 & m4f1);
      end
end

Сигналы { ms0, ms1 } управляют счетчиком:

  • 2’b00 — переход на цикл m1,

  • 2’b01 — переход на следующий цикл (нельзя на m5),

  • 2’b10 — сохранение текущего цикла,

  • 2’b11 — переход к циклу m4 (допустимо в m1).

Сигналы { ms0, ms1 } формируются по следующим правилам:

assign ms0 = ~reset & m839 & ~(sy_stack & ~t1f1) & ~(eom & ~m836);
assign ms1 = ~reset & m839 & ~(sy_stack & ~t1f1) & ~(m871 & ~m836) & eom;

assign m836 = m1f1 & id_skip_m2_m3;
assign m839 = ~t976 | ~sy_hlta;
assign m871 = t789 | id_1cycle;

always @(posedge clk)
begin
   if (f1)
      begin
         t789 <= (id_3cycle & m3) | (id_4cycle & ~id_mvim & m4) | m5;
         t976 <= t980 & m4;
      end
   if (f2)
      begin
         t980 <= sy_inta;
      end
end
  • когда активен reset: переход на m1,

  • eom == 0: текущий цикл не меняется,

  • eom == 1: обычно переход на следующий цикл,

  • другие варианты для случая eom == 1:

    • eom & id_1cycle: переход на m1 для выполнения следующей инструкции,

    • eom & m1f1 & id_skip_m2_m3: пропуск циклов m2 и m3,

    • eom & m3 & id_3cycle: переход на m1,

    • eom & m4 & id_4cycle & ~id_mvim: переход на m1,

    • eom & m5: переход на m1,

  • когда sy_stack == 1 (на t3 или t5): переход на m1.

(Прерывания и прямой доступ к памяти исключим из рассмотрения.)

Переключением этапов машинного цикла управляет соответствующий счетчик:

always @(posedge clk)
begin
   if (f1)
      begin
         t1f1 <= t1 & ~reset; // ensure the reliable start after reset
         t2f1 <= t2;
         twf1 <= tw;
         t3f1 <= t3;
         t4f1 <= t4;
         t5f1 <= t5;
      end
   if (f2)
      begin
         t1 <= start;
         t2 <= ~start & t1f1;
         tw <= ~start & (t2f1 | twf1) & (~ready | thalt);
         t3 <= ~start & (t2f1 | twf1) &  ready & ~thalt;
         t4 <= ~start & t3f1 & ms0 & ~ms1;
         t5 <= ~start & t4f1 & ms0 & ~ms1;
      end
end

Т.е. обычно при наступлении тактового сигнала f1 происходит переход к следующему этапу.

Отметим, что сигналы, например, t2f1 и t2 указывают на один и тот же этап, но смещены во времени: t2 наступает раньше, уже во время тактового сигнала f2, а t2f1 наступает позже во время тактового сигнала f1:

такты машинного цика
такты машинного цика
assign start = ~m839
             | t953
             | (eom & ~(hold & t887))
             | (f2 & ((~t382 & hold) | t383) & ~(twf1 | t3f1 | t4f1 | t5f1));

always @(posedge clk)
begin
   if (f1)
      begin
         t887 <= hold;
         t953 <= reset;
      end
end
  • когда активен reset: переход на t1,

  • eom == 1: переход на t1 — начальный этап для следующего машинного цикла.

Конец машинного цикла определяется следующим образом:

always @(posedge clk)
begin
   if (f1)
      begin
         eom <= t5
              | t4 & m1 & id_m1_4state
              | t3 & m2
              | t3 & m3
              | t3 & m4
              | t3 & m5 & ~id_xthl;
      end
end

Таким образом, максимально может быть такой набор машинных циклов:

Цикл

Такты

m1

t1, t2, tw, t3, t4, t5

m2

t1, t2, tw, t3

m3

t1, t2, tw, t3

m4

t1, t2, tw, t3

m5

t1, t2, tw, t3, t4, t5

Пины данных и адреса

Приведем полностью мультиплексор на входе шины данных d:

wire [7:0] d;
assign d    = ~reset & ~alu_00_TO_data &
               ( dbin_ext & di
               | r16_val_low_TO_data & r16_val
               | r16_val_high_TO_data & r16_val
               | t1f1 & { sy_memr, sy_inp, sy_m1, sy_out, sy_stack, sy_wo_n, sy_inta }
               | alu_xr_TO_data & xr
               | alu_acc_TO_data & acc
               | alu_psw_TO_data & psw_s
               | alu_s_TO_data & s );

В обычном состоянии процессора на пины адреса pin_a всегда выводится шина адреса a.

На цикле чтения (когда sy_wo_n равен 1) выполняется чтение с пинов данных pin_din на шину данных d следующим образом:

reg [7:0] di;

assign d    = //...
               | dbin_ext & di
               //...
               ;

always @(posedge clk)
begin
   if (dbin_pin) di <= pin_din;
   
   if (f1) t1124 <= (t2 | tw) & sy_wo_n;
   if (f2) dbin_pin <= t1124 & (m1f1 | ~sy_hlta);
   dbin_ext <= (f2 | dbin_pin) & ((t1124 & (m1f1 | ~sy_hlta)) | dbin_ext);
end

Если упростить, то значение с pin_din сохраняется на защелке di на такте t3 и выводится на d начиная с t3 и до t4f1.

На цикле записи (когда sy_wo_n равен 0) значение с шины данных d выводится на пины данных pin_dout следующим образом:

reg [7:0] db

assign pin_dout   = db;
assign pin_dena   = db_ena;

always @(posedge clk)
begin
   if (f1) db_stb <= t1 | (~sy_wo_n & t2);
   if (f2 & db_stb) db <= d;
   
   if (f1) t851 <= t1 | (~sy_wo_n & (t2 | tw | t3));
   if (f2) db_ena <= ~reset & t851;
end

Т.е. значение с шины данных d сохраняется на защелке db на такте t3 и выводится на pin_dout во время t3 и t4.

Порты ввода pin_din и вывода pin_dout из модуля vm80a_core объединяются в один двунаправленный порт pin_d в модуле vm80a.

Машинные циклы m1, m2 и m3 — циклы чтения, а m4 и m5 могут быть циклами записи, в зависимости от инструкции:

assign sy_wo_n  = m1 | m2 | m3 | (((m4 & ~id_write_on_m4) | (m5 & ~id_write_on_m5)) & ~ready_int);

Обратим внимание, что сигнал sync активен на такте t2:

always @(posedge clk)
begin
   if (f2) sync <= ~ready_int & t1011;
   if (f1) t1011 <= t1 & ~reset;
end

Поэтому номера тактов в даташите и в коде не совпадают: номер такта в коде на единицу больше.

Также по коду видно, что в основном используются сигналы t1, …, t5, а не t1f1, …, t5f1. Поэтому справедливее сказать, что начало такта совпадает с сигналом f2, но в даташите началом такта обознчен сигнал f1.

Управляющие сигналы для регистрового файла

Код, отвечающий за формирование управляющих сигналов, надо изучать с такой точки зрения: на каких тактах каких машинных циклов они активируются.

r16_val_TO_addr

assign      r16_val_TO_addr   = t3363 | (t4f1 & ~id_dad & ~id_hlt);

always @(posedge clk)
begin
   if (f1)
      begin
         t3363 <= t1 & (m1 | m2 | m3 | ((m4 | m5) & ~id_hlt & ~id_dad));
      end
end

Т.е. сигнал r16_val_TO_addr выдается на такте t1 и t4.f1 всех машинных циклов (с исключениями для некоторых инструкций).

addr_plus_TO_r16

assign      addr_plus_TO_r16    = t3403;

always @(posedge clk)
begin
   if (f1)
      begin
         t3403 <= t2 & ( (m1 | m2)
                       | (m3 & ~id_xthl)
                       | ((m4 | m5) & ~id_dad & ~id09))
                | t3 & m4 & id09
                | t5 & (m5 | (m1 & ~id08));
      end
end

Т.е. сигнал addr_plus_TO_r16 выдается

  • на такте t2 всех машинных циклов (с исключениями для некоторых инструкций и id09),

  • на m4.t3 у инструкций из id09,

  • на m1.t5, кроме инструкций из id08,

  • на m5.t5.

Разбор инструкций

Из-за того что логика управляющих сигналов из описания процессора на языке Verilog не очевидна (мягко говоря), далее опишем для некоторых инструкций, на каких тактах каких машинных циклов какие управляющие сигналы выставляются.

В графе «характеристики» указаны классы, к которым инструкция принадлежит:

  • id_1cycle, id_4cycle, id_3cycle — номер последнего машинного цикла инструкции,

  • id_skip_m2_m3 — после выполнения цикла 1 осуществляется переход на машинный цикл 4,

  • id_m1_4state — первый машинный цикл состоит из четырех тактов (без этого класса — из пяти тактов),

  • id_write_on_m4, id_write_on_m5 — машинный цикл записи в память (или порт ввода-вывода).

Чтение первого байта инструкции выполняется на m1. Если инструкция двухбайтовая, то m2 и m3 пропускаются, а второй байт прочитывается на m4. Если инструкция трехбайтовая, то второй и третий байт прочитываются на m2 и m3.

На m1, m2 и m3 происходит инкремент PC. Если это нужно на m4, то инструкция должна принадлежать классу id02. Для инкремента активен сигнал inc16. На такте t1 выдаются сигналы r16_val_TO_addr и sel_pc. На такте t2 выдаются сигналы addr_plus_TO_r16 и sel_pc.

Сочетание r16_val_TO_addr и sel_pc приводит, конечно, к тому, что на шине адреса появляется адрес очередного байта инструкции. На такте m1.t3.f1_ext код инструкции сохраняется в регистре инструкции i:

always @(posedge clk)
begin
   if (~f2 & (reset | (m1 & t3))) i <= pin_din;
end

Отметим, что регистр инструкции меняется только на такте t3, поэтому на t1 и t2 первого машинного цикла выполняется предыдущая инструкция. Часть инструкций эту фишку используют (арифметические, логические, переход).

xchg

Спецификация: обмен содержимого DE и HL.

Характеристики: id_m1_4state, id_1cycle.

Эта инструкция реализована интересным образом: а именно — просто переименовывает DE и HL. Для этого используется флаг xchg_dh, который меняет местами sel_hl и sel_de.

Переключается xchg_dh в конце такта t2 первого машинного цикла следующей инструкции:

always @ (posedge clk)
begin
      xchg_tt <= id_xchg & t2;
      xchg_dh <= ~reset & ((xchg_tt & ~(id_xchg & t2)) ? ~xchg_dh : xchg_dh);
end

mov r1, r2

Спецификация: r1 := r2.

Характеристики: id_1cycle, id08.

Циклы: m1.

Отметим, что для временного хранения 8-битного значения в этой и других инструкциях используется регистр xr из АЛУ.

Цикл m1:

Такт

Активные сигналы

Описание

t4

imx = 0

sel_* выбирает регистр из младших битов инструкции

sel_*, r16_val_high/low_TO_data (от id08)

d = r16_*[high/low]

alu_data_TO_xr

xr := d

t5

alu_xr_TO_data

d = xr

sel_*, data_TO_r16_high/low (от id08)

r16_*[high/low] := d

Отметим, что если в качестве регистра-источник указан A, то в этой и других инструкциях вместо r16_val_high/low_TO_data будет сигнал alu_acc_TO_data, чтобы на шине данных было значение аккумулятора.

Поэтому цикл m1 для “mov r, a” следующий:

Такт

Активные сигналы

Описание

t4

imx = 0

sel_* выбирает регистр из младших битов инструкции

acc_sel, alu_acc_TO_data (от id08)

d = acc

alu_data_TO_xr

xr := d

t5

alu_xr_TO_data

d = xr

sel_*, data_TO_r16_high/low (от id08)

r16_*[high/low] := d

Отметим, что если в качестве регистра-назначения указан A, то в этой и других инструкциях вместо data_TO_r16_high/low будет сигнал alu_data_TO_acc, чтобы в аккумулятор записалось значение из шины данных.

Поэтому цикл m1 для “mov a, r” следующий:

Такт

Активные сигналы

Описание

t4

imx = 0

sel_* выбирает регистр из младших битов инструкции

sel_*, r16_val_high/low_TO_data (от id08)

d = r16_*[high/low]

alu_data_TO_xr

xr := d

t5

alu_xr_TO_data

d = xr

acc_sel, alu_data_TO_acc (от id08)

acc := d

mov m, r

Спецификация: memory[HL] := r.

Характеристики: id_m1_4state, id_skip_m2_m3, id_4cycle, id_write_on_m4, id08.

Циклы: m1, m4.

Цикл m1:

Такт

Активные сигналы

Описание

t4

imx = 0

sel_* выбирает регистр из младших битов инструкции

sel_*, r16_val_high/low_TO_data (от id08)

d = r16_*[high/low]

alu_data_TO_xr

xr := d

Цикл m4:

Такт

Активные сигналы

Описание

t1

r16_val_TO_addr, sel_hl

a := r16_hl

t2

sy_wo_n = 0 (от id_write_on_m4)

Режим записи

alu_xr_TO_data

d = xr

db_stb (на f2)

db := d

db_ena (на f2)

pin_d = db

t3

sy_wo_n = 0 (от id_write_on_m4)

Режим записи

db_ena

pin_d = db

mov r, m

Спецификация: r := memory[HL].

Характеристики: id_m1_4state, id_skip_m2_m3, id_4cycle, id08, id10.

Циклы: m1, m4.

Цикл m4:

Такт

Активные сигналы

Описание

t1

r16_val_TO_addr, sel_hl

a := r16_hl

t3

db_ena = 0, dbin_pin, sy_wo_n

Режим чтения

dbin_pin (на f1)

di := pin_din

dbin_ext

d = di

sel_* (от id10), data_TO_r16_high/low (от id08)

r16_*[high/low] := d

lda addr

Спецификация: A := memory[addr].

Характеристики: id_m1_4state, id_4cycle.

Циклы: m1, m2, m3, m4.

Отметим, что для временного хранения 16-битного значения в этой и других инструкциях используется регистр WZ.

Цикл m2:

Такт

Активные сигналы

Описание

t1

sync

db_ena

r16_val_TO_addr, sel_pc

a := r16_pc

t2

addr_plus_TO_r16, sel_pc

r16_pc := a + 1

db_ena = 0, dbin_pin, sy_wo_n

Режим чтения

t3

db_ena = 0, dbin_pin, sy_wo_n

Режим чтения

dbin_pin (на f1)

di := pin_din

dbin_ext

d = di

data_TO_r16_low, sel_wz

r16_wz[7:0] := d

Цикл m3:

Такт

Активные сигналы

Описание

t1

sync

db_ena

r16_val_TO_addr, sel_pc

a := r16_pc

t2

addr_plus_TO_r16, sel_pc

r16_pc := a + 1

db_ena = 0, dbin_pin, sy_wo_n

Режим чтения

t3

db_ena = 0, dbin_pin, sy_wo_n

Режим чтения

dbin_pin (на f1)

di := pin_din

dbin_ext

d = di

data_TO_r16_high, sel_wz

r16_wz[15:8] := d

Цикл m4:

Такт

Активные сигналы

Описание

t1

r16_val_TO_addr, sel_wz

a := r16_wz

t3

db_ena = 0, dbin_pin, sy_wo_n

Режим чтения

dbin_pin (на f1)

di := pin_din

dbin_ext

d = di

alu_data_TO_acc

acc := d

adi val

Спецификация: A := A + val.

Характеристики: id_m1_4state, id_skip_m2_m3, id_4cycle, id02.

Циклы: m1, m4.

Отметим, что сохранение результата в аккумулятор и сохранение флагов в этой и других арифметико-логических инструкциях происходит на такте t2 цикла m1 следующей инструкции.

Цикл m4:

Такт

Активные сигналы

Описание

t1

sync

db_ena

r16_val_TO_addr, sel_pc (от id02)

a := r16_pc

t2

addr_plus_TO_r16, sel_pc (от id02)

r16_pc := a + 1

db_ena = 0, dbin_pin, sy_wo_n

Режим чтения

t3

db_ena = 0, dbin_pin, sy_wo_n

Режим чтения

dbin_pin (на f1)

di := pin_din

dbin_ext

d = di

alu_acc_TO_r

r := acc

alu_data_TO_xr

xr := d

id_add

tmp_c := 0

cl = tmp_c
s = (x + r + cl)

Цикл m1 следующей инструкции:

Такт

Активные сигналы

Описание

t2

alu_s_TO_acc

acc := s

res_TO_psw

сохранение флагов

aci val

Спецификация: A := A + val + psw.carry.

Отличия от aci:

  • на m1.t3 выполняется tmp_c := psw_c

  • на m4.t4 нет обнуления tmp_c := 0

sui val

Спецификация: A := A - val.

Отличия от adi:

  • активен alu_xr_TO_x_inv: x = ~xr

  • на m4.t4 выполняется tmp_c := 1

(т.е. второй аргумент преобразуется в дополнительный код).

Также на m1.t2 флаг переноса сохраняется в инвертированном виде (как заимствование).

inr r

Спецификация: r := r + 1.

Характеристики: id_1cycle, id08.

Циклы: m1.

Цикл m1:

Такт

Активные сигналы

Описание

t3

alu_00_TO_data, alu_data_TO_r

r := 0

t4

sel_*, r16_val_high/low_TO_data (от id08)

d = r16_*[high/low]

alu_data_TO_xr

xr := d

id_inr

tmp_c := 1

t5

alu_s_TO_data

d = s

data_TO_r16_high/low, sel_* (от id08)

r16_*[high/low] := d

cl = tmp_c
s = (x + r + cl)

Цикл m1 следующей инструкции:

Такт

Активные сигналы

Описание

t2

res_TO_psw

сохранение флагов

inx r16

Спецификация: r16 := r16 + 1.

Характеристики: id_1cycle, id07.

Циклы: m1.

Для увеличения значения регистровой пары используется 16-битный инкрементер (который используется для инкремента PC).

Цикл m1:

Такт

Активные сигналы

Описание

t4

sel_* (от id07), r16_val_TO_addr

a := r16_*

t5

sel_* (от id07), addr_plus_TO_r16

r16_* := a + 1

ana val

Спецификация: A := A & val.

Характеристики: id_m1_4state, id_1cycle, id08.

Циклы: m1.

Цикл m1:

Такт

Активные сигналы

Описание

t4

alu_acc_TO_r

r := acc

sel_*, r16_val_high/low_TO_data (от id08)

d = r16_*[high/low]

alu_data_TO_xr

xr := d

id_ana

tmp_c := 1

s = (x & r)

Цикл m1 следующей инструкции:

Такт

Активные сигналы

Описание

t2

alu_s_TO_acc

acc := s

res_TO_psw

сохранение флагов (psw_c := ~tmp_c)

jmp addr

Спецификация: PC := addr.

Характеристики: goto, id_m1_4state, id_3cycle.

Циклы: m1, m2, m3.

Цикл m2:

Такт

Активные сигналы

Описание

t1

sync

db_ena

r16_val_TO_addr, sel_pc

a := r16_pc

t2

addr_plus_TO_r16, sel_pc

r16_pc := a + 1

db_ena = 0, dbin_pin, sy_wo_n

Режим чтения

t3

db_ena = 0, dbin_pin, sy_wo_n

Режим чтения

dbin_pin (на f1)

di := pin_din

dbin_ext

d = di

data_TO_r16_low, sel_wz

r16_wz[7:0] := d

Цикл m3:

Такт

Активные сигналы

Описание

t1

sync

db_ena

r16_val_TO_addr, sel_pc

a := r16_pc

t2

addr_plus_TO_r16, sel_pc

r16_pc := a + 1

db_ena = 0, dbin_pin, sy_wo_n

Режим чтения

t3

db_ena = 0, dbin_pin, sy_wo_n

Режим чтения

dbin_pin (на f1)

di := pin_din

dbin_ext

d = di

data_TO_r16_high, sel_wz

r16_wz[15:8] := d

Цикл m1 следующей инструкции берет адрес из wz:

Такт

Активные сигналы

Описание

t1

sync

db_ena

r16_val_TO_addr, sel_wz

a := r16_wz

t2

addr_plus_TO_r16, sel_pc

r16_pc := a + 1

db_ena = 0, dbin_pin, sy_wo_n

Режим чтения

j cond addr

Спецификация: if (cond) then PC := addr.

Характеристики: goto, id_m1_4state, id_3cycle.

Циклы: m1, m2, m3.

Вычисление условия срабатывания перехода:

assign jmpflag =    (psw_c &  i14 & ~i25)
                  | (psw_p & ~i14 &  i25)
                  | (psw_z & ~i14 & ~i25)
                  | (psw_s &  i14 &  i25);

Цикл m1:

Такт

Активные сигналы

Описание

t4

jmptake <= i03 ? jmpflag : ~jmpflag

Цикл m2:

Такт

Активные сигналы

Описание

t1

sync

db_ena

r16_val_TO_addr, sel_pc

a := r16_pc

t2

addr_plus_TO_r16, sel_pc

r16_pc := a + 1

db_ena = 0, dbin_pin, sy_wo_n

Режим чтения

t3

db_ena = 0, dbin_pin, sy_wo_n

Режим чтения

dbin_pin (на f1)

di := pin_din

dbin_ext

d = di

data_TO_r16_low, sel_wz

r16_wz[7:0] := d

Цикл m3:

Такт

Активные сигналы

Описание

t1

sync

db_ena

r16_val_TO_addr, sel_pc

a := r16_pc

t2

addr_plus_TO_r16, sel_pc

r16_pc := a + 1

db_ena = 0, dbin_pin, sy_wo_n

Режим чтения

t3

db_ena = 0, dbin_pin, sy_wo_n

Режим чтения

dbin_pin (на f1)

di := pin_din

dbin_ext

d = di

data_TO_r16_high, sel_wz

r16_wz[15:8] := d

Цикл m1 следующей инструкции берет адрес из wz, если jmptake:

Такт

Активные сигналы

Описание

t1

sync

db_ena

r16_val_TO_addr, jmptake ? sel_wz : sel_pc

a := r16_wz/pc

t2

addr_plus_TO_r16, sel_pc

r16_pc := a + 1

db_ena = 0, dbin_pin, sy_wo_n

Режим чтения

Что касается инструкций условного перехода и условного возврата, то в них используется сигнал sy_stack, чтобы пропустить часть инструкции (добавление/извлечение адреса возврата из стека), когда условие не выполнено:

always @(posedge clk)
begin
   if (f1)
      begin
         sy_stack <= (t1 & t1460)
                   | (t3 & m3 & id_cxx & ~jmptake)
                   | (t5 & m1 & id_rxx & ~jmptake);
      end
end

push r16

Спецификация: memory[SP-1] := r16_high; memory[SP-2] := r16_low; SP := SP-2.

Характеристики: id_skip_m2_m3, id_write_on_m4, id_write_on_m5, id03, id04, id05, id09, id10.

Циклы: m1, m4, m5.

Для уменьшения значения указателя стека используется 16-битный декрементер.

Цикл m1:

Такт

Активные сигналы

Описание

t4

r16_val_TO_addr, sel_sp (от id04)

a := r16_sp

t5

addr_plus_TO_r16, sel_sp (от id04), dec16 (от id05)

r16_sp := a - 1

Цикл m4:

Такт

Активные сигналы

Описание

t1

r16_val_TO_addr, sel_sp (от id04)

a := r16_sp

t2

sy_wo_n = 0 (от id_write_on_m4)

Режим записи

r16_val_high_TO_data (от id03), sel_* (от id10)

d = r16_*[15:8]

db_stb (на f2)

db := d

db_ena (на f2)

pin_d = db

t3

addr_plus_TO_r16 (от id09), sel_sp (от id04), dec16 (от id05)

r16_sp := a - 1

sy_wo_n = 0 (от id_write_on_m4)

Режим записи

db_ena

pin_d = db

Цикл m5:

Такт

Активные сигналы

Описание

t1

r16_val_TO_addr, sel_sp (от id04)

a := r16_sp

t2

sy_wo_n = 0 (от id_write_on_m5)

Режим записи

r16_val_low_TO_data (от id03), sel_* (от id10)

d = r16_*[7:0]

db_stb (на f2)

db := d

db_ena (на f2)

pin_d = db

t3

sy_wo_n = 0 (от id_write_on_m5)

Режим записи

db_ena

pin_d = db

push psw

Отличия цикла m4 для “push psw”: вместо r16_val_high_TO_data будет сигнал alu_acc_TO_data, чтобы на шине данных было значение аккумулятора.

Отличия цикла m5 для “push psw”: вместо r16_val_low_TO_data будет сигнал alu_psw_TO_data, чтобы на шине данных были значения флагов.

pop r16

Спецификация: r16_low := memory[SP]; r16_high := memory[SP+1]; SP := SP+2.

Характеристики: id_m1_4state, id_skip_m2_m3, id01, id06, id10.

Циклы: m1, m4, m5.

Цикл m4:

Такт

Активные сигналы

Описание

t1

r16_val_TO_addr, sel_sp (от id01)

a := r16_sp

t2

addr_plus_TO_r16, sel_sp (от id01)

r16_sp := a + 1

t3

db_ena = 0, dbin_pin, sy_wo_n

Режим чтения

dbin_pin (на f1)

di := pin_din

dbin_ext

d = di

sel_* (от id10), data_TO_r16_low (от id06)

r16_*[7:0] := d

Цикл m5:

Такт

Активные сигналы

Описание

t1

r16_val_TO_addr, sel_sp (от id01)

a := r16_sp

t2

addr_plus_TO_r16, sel_sp (от id01)

r16_sp := a + 1

t3

db_ena = 0, dbin_pin, sy_wo_n

Режим чтения

dbin_pin (на f1)

di := pin_din

dbin_ext

d = di

sel_* (от id10), data_TO_r16_high (от id06)

r16_*[15:8] := d

pop psw

Отличия цикла m4 для “pop psw”: вместо data_TO_r16_low будет сигнал data_TO_psw, чтобы значения флагов записались из шины данных.

Отличия цикла m5 для “pop psw”: вместо data_TO_r16_high будет сигнал alu_data_TO_acc, чтобы в аккумулятор записалось значение из шины данных.

Заключение

Достаточно сложно изучать описание, полученное реверс-инжинирингом. На более низких уровнях реализации теряются высокоуровневые понятия и концепции, и возникают трудности по их восстановлению.

Самым полезным подспорьем оказалось знание матчасти – микроархитектуры простых процессоров с общей шиной, разделение на операционную и управляющую часть, машинные циклы. Далее, предположив из спецификации логику работы инструкции, можно сузить набор управляющих сигналов для операционной части и изучить их изменение в симуляторе verilog.

Прорывом в понимании стало осознание, что код, отвечающий за формирование управляющих сигналов, описывает их связь с тактами машинных циклов.

Интересно было узнать, что инструкция xchg просто переименовыает регистровые пары. Также реализация условых переходов и вызовов подпрограмм оказалась не очевидной для меня. Неожиданно выяснилось, что окончание выполнения инструкции в процкссоре 8080 может залезать на начало первого машинного цикла следующей инструкции.

Выражаем благодарность энтузиастам реверс-инжиниринга — благодаря их труду доступны даже исходники процессоров.

Комментарии (0)