Некоторое время назад стало доступно описание на языке 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 может залезать на начало первого машинного цикла следующей инструкции.
Выражаем благодарность энтузиастам реверс-инжиниринга — благодаря их труду доступны даже исходники процессоров.