Оглавление


§ Модификация кода

Традиционно, перед началом работы внесу определенные улучшающие модификации в код. В предыдущей главе в инструкциях INX и DCX использовался громоздкий case для выбора регистра. Чтобы избежать лишних строк кода, вынесу выбор 16-битного регистра вне блока always:
1wire [ 7:0] opc = t ? opcode : in;
2wire [15:0] r16 =
3    opc[5:4] == 2'b00 ? bc :
4    opc[5:4] == 2'b01 ? de :
5    opc[5:4] == 2'b10 ? hl : sp;
Помимо выбора регистра 16-бит, также выбирается и opc — текущий опкод, который доступен с любого t. Также для дальнейших целей нам потребуется еще выбор 8-битного регистра точно таким же методом, на диапазоне [5:3] опкода.
1wire [ 7:0] op53 =
2    opc[5:3] == 3'b000 ? bc[15:8] : opc[5:3] == 3'b001 ? bc[ 7:0] :
3    opc[5:3] == 3'b010 ? de[15:8] : opc[5:3] == 3'b011 ? de[ 7:0] :
4    opc[5:3] == 3'b100 ? hl[15:8] : opc[5:3] == 3'b101 ? hl[ 7:0] :
5    opc[5:3] == 3'b110 ? in       : a;
Далее потребуется также выбор регистра на диапазоне [2:0].
1wire [ 7:0] op20 =
2    opc[2:0] == 3'b000 ? bc[15:8] : opc[2:0] == 3'b001 ? bc[ 7:0] :
3    opc[2:0] == 3'b010 ? de[15:8] : opc[2:0] == 3'b011 ? de[ 7:0] :
4    opc[2:0] == 3'b100 ? hl[15:8] : opc[2:0] == 3'b101 ? hl[ 7:0] :
5    opc[2:0] == 3'b110 ? in       : a;
Достаточно много мультиплексоров, но в целом, они покрывают все нужны процессора в выборе регистров. Как можно отметить, в выборе источника входа вместо регистра фигурирует in, который является выбором источника "память" (M).

§ Инструкция MVI

Пересылает байт, идущий за опкодом, либо в регистр (MVI R,*), либо в память (MVI M,*). Указатель памяти — регистровая пара HL. Здесь код можно сделать двумя способами — либо обрабатывать каждый случай отдельно, для регистра и памяти, либо сделать запись в память или регистр совместно.
Добавлю еще один полезный проводок в схему.
1wire m53 = (opc[5:3] == 3'b110);
2wire m20 = (opc[2:0] == 3'b110);
Это будет необходимо для определения метода адресации. Вместо указания регистра можно указывать адрес в памяти, который является M. Для того, чтобы это знать, у окпода может быть в битах 5:3 либо 110b, либо в битах 2:0, в зависимости от выполняемой инструкции. Для MVI используется диапазон битов 5:3 у опкода.
18'b00xx_x110: case (t)
2
3    1: begin
4
5        pc  <= pcn;      // PC = PC + 1
6        cp  <= hl;       // Указатель HL
7        n   <= opc[5:3]; // Номер регистра
8        b   <= !m53;     // Запись в регистр, если не M
9        we  <= m53;      // Запись в память,  если M
10        sw  <= m53;      // Активация указателя CP
11        d   <= in;       // Данные для записи в регистр
12        out <= in;       // Данные для записи в память
13
14    end
15    6: begin t <= sw ? 7 : 0; sw <= 0; end
16    9: begin t <= 0; end
17
18endcase
Поскольку я объединил запись как в регистр, так и в память, то и количество тактов у них разное. При записи в регистр количество тактов равно 7, а при записи в память будет 10 тактов.
Так как код достаточно хорошо документирован, я не буду описывать его работу подробно. Хитрость в том, что когда выбран режим M (запись в память), то вместо b=1 становится b=0, we=1; то есть, данные пишутся именно в память. При записи в регистр будет b=1, we=0, и запись будет в регистр. Инструкция достаточно простая и на самом деле, выполняется всего лишь за 3 такта.

§ Инструкция SHLD и LDHL

Эти две инструкции выполняют либо запись HL в 16-битный указатель памяти (SHLD addr), либо, наоборот, чтение из памяти в HL (LDHL addr). Каждая из этих инструкции выполняется за 16 тактов. Это одна из самых ресурсоёмких инструкции. Честно признаться, такое невероятное "быстродействие" немного удручает, но так как решил полностью следовать тактам, то буду им следовать. Программы должны тормозить.
Рассмотрим код SHLD.
18'b0010_0010: case (t)
2
3    1: begin cp[ 7:0] <= in; pc <= pcn; end
4    2: begin cp[15:8] <= in; pc <= pcn; sw <= 1; end
5    3: begin we <= 1; out <= hl[ 7:0]; end
6    4: begin we <= 1; out <= hl[15:8]; cp <= cpn; end
7    15: begin sw <= 0; t <= 0; end
8
9endcase
  • На такте 1 и 2 считывается адрес, и на 2-м также этот адрес выбирается вместо PC
  • На такте 3 и 4 записываются последовательно запись сначала L регистра в память, а потом H. А также, в CP пишется значение CPN (CP+1)
1wire [15:0] cpn = cp + 1;
  • На 15-м такте включается опять указатель на PC
В общем-то, ничего сложного. Инструкция LDHL почти что идентична:
18'b0010_1010: case (t)
2
3    1: begin cp[ 7:0] <= in; pc <= pcn; end
4    2: begin cp[15:8] <= in; pc <= pcn; sw <= 1; end
5    3: begin d [ 7:0] <= in; cp <= cpn; end
6    4: begin d [15:8] <= in; w <= 1; n <= 2; sw <= 0; end
7    15: begin t <= 0; end
8
9endcase
Вначале происходит выборка указателя CP, и далее последовательно читаются данные в 16-битный регистр d и на такте #4 записываются в n=2 (это HL).

§ Инструкция STA, LDA

Эти две инструкции по своей сути разные, STA записывает содержимое A в память по 16-битному адресу, а LDA считывает. Каждая из них выполняется за 13 тактов.
18'b0011_x010: case (t)
2
3    1: begin cp[ 7:0] <= in; pc <= pcn; end
4    2: begin cp[15:8] <= in; pc <= pcn; sw <= 1; we <= ~opc[3]; out <= a; end
5    3: begin d <= in; b <= opc[3]; n <= 7; sw <= 0; end
6    12: begin t <= 0; end
7
8endcase
Несмотря на то что инструкции разные, я смог объединить их в одну.
  • Первые два такта (1 и 2) происходит считывание адреса в CP указатель
  • На 2-м такте переключается на этот адрес, на всякий случай пишется A в out и если опкод равен 32h, то пишется в память we=1, но если опкод 3Ah, то не пишет
  • На 3-м такте на всякий случай записывается в d, и аналогично предыдущему такту, если опкод 3Ah, то записывает в регистр A (n=7), и sw=0 обратно включает указатель PC в памяти
Поэтому, мне не пришлось делать две инструкции, так как легко получилось сделать в одной через вариацию opc[3].

§ Инструкции RLC, RRC, RAL, RAR (сдвиги)

В отличии от спектрумского Z80, инструкции сдвига тут всего лишь 4, и они только для аккумулятора. Это очень небольшой набор. Поскольку все эти 4 инструкции по сути однотипные и выполняются за 4Т, то я их объединил в один набор.
18'b000x_x111: case (t)
2
3    0: begin
4
5        b <= 1;
6        n <= 7;
7
8        case (opc[4:3])
9        2'b00: d <= {a[6:0], a[7]};   // RLC
10        2'b01: d <= {a[0], a[7:1]};   // RRC
11        2'b10: d <= {a[6:0], psw[0]}; // RAL
12        2'b11: d <= {psw[0], a[7:1]}; // RAR
13        endcase
14
15        psw[0] <= a[opc[3] ? 0 : 7];
16
17    end
18    3: begin t <= 0; end
19
20endcase
В зависимости от того, какая инструкция, будет записан флаг CF. Если это инструкция RLC или RAL, то в CF копируется 7-й бит из регистра A, поскольку эти инструкции — сдвиг влево. В случае если инструкция RRC или RAR, то наоборот, из бита 0 регистра A копируется в CF.
  • RLC смещает младшие 7 битов [6:0] на бит влево, перемещая бит 7 в бит 0. Это дает "вращение" влево
  • RRC смещает старшие 7 битов [7:1] на бит вправо, перемещая бит 0 в бит 7. Это дает "вращение" вправо
  • RAL тоже как и RLC, смещает биты влево, но вместо копирования 7-го бита, берет бит из CF (Carry Flag)
  • RAR аналогичен RRC, за исключением, что в старший бит вдвигается значение CF
Соответственно, полученный результат записывается в d, а оттуда в регистр A на следующем такте.

§ Инструкции CMA, STC и CMC

Достаточно простые инструкции. CMA просто инвертирует биты в регистре A, при этом не меняя никаких флагов. STC устанавливает CF=1, а CMC же инвертирует бит CF. Реализация CMA:
18'b0010_1111: case (t)
2
3    0: begin d <= ~a; b <= 1; n <= 7; end
4    3: begin t <= 0; end
5
6endcase
Реализация STC и CMC.
18'b0011_x111: case (t)
2
3    0: begin psw[0] <= opc[3] ? /*CMC*/ ~psw[0] : /*STC*/ 1'b1; end
4    3: begin t <= 0; end
5
6endcase
Комментарии мне кажется, излишни тут.

§ Инструкция DAD

Сложение регистровой пары HL с BC, DE, HL или SP и запись в HL результата. При переполнении 16-битного результата устанавливается CF=1. Инструкция выполняется 10 тактов.
18'b00xx_1001: case (t)
2
3    0: begin d <= hl + r16; w <= 1; n <= 2; end
4    1: begin psw[0] <= d[16]; end
5    9: begin t <= 0; end
6
7endcase
На #0 такте происходит как сложение результата, так и выбирается запись (w=1) в регистр HL (n=2). На следующем такте записывается 16-й бит переноса во флаг CF, или psw[0].

§ Инструкции INR, DCR

Эти две инструкции представляют из себя инкремент (INR) и декремент (DCR) регистра или 8-битного значения в памяти. Есть возможность уместить в одном коде также выборку из памяти, и также из регистров. Ниже приведен код, и далее разберу подробнее, как он работает.
18'b00xx_x10x: case (t)
2
3    0: begin cp <= hl; sw <= 1; end
4    1: begin d <= opc[0] ? op53 - 1 : op53 + 1; end
5    2: begin
6
7        psw[SF] <= d[7];
8        psw[ZF] <= d[7:0] == 0;
9        psw[HF] <= d[3:0] == (opc[0] ? 4'hF : 4'h0);
10        psw[PF] <= ~^d[7:0];
11
12        n   <= opc[5:3];
13        b   <= ~m53;        // Либо в регистр запись
14        we  <= m53;         // Либо запись в память
15        out <= d;
16
17    end
18    4: begin sw <= 0; t <= m53 ? 5 : 0; end
19    9: begin t <= 0; end
20
21endcase
Алгоритм работы
  • Такт 0 устанавливает указатель адреса в памяти на HL (указываем sw=1). Это потребуется для того случая, если нужно будет прочесть значение из памяти
  • Такт 1 непосредственно вычисляет либо +1, либо -1, в зависимости от опкода [INR или DCR]
  • Такт 2 предназначен для записи результата в флаги и также в регистр/память.
Из результата инкремента или декремента копируется бит 7-й в SF. Вычисляется ZF=1, если результат получился 0, и PF=1, если количество битов четно. Для HF устанавливается результат =1 в зависимости от того, инкремент это был или декремент. При инкременте HF=1 равен тогда и только тогда, когда младший ниббл оказался равен 0, потому что предыдущее его значение было гарантированно равно F. С декрементом ситуация как раз наоборот, если младший ниббл результата F, то это значит, что был заем из бита 4.
На этом же такте делается выбор, куда писать. Либо в регистр, либо в память, и соответственно в out устанавливается результат, при случае, если будет произведена запись в память.
  • Такт 4 — это на самом деле, 5-й такт, выбирается случай, если была операция с регистром, то инструкция завершается, а если с памятью, то продолжается еще 2Т, тем самым выполняясь за 7Т вместо 5Т.

§ Инструкция DAA

Могу сказать, что это одна из самых запутанных инструкции во всем процессоре, да и вообще, везде. Эта инструкция корректирует результат двоично-десятичного значения в байте после сложения. Вычитание тут не предусмотрено.
Для начала разберемся что такое двоично-десятичное представление числа. В одном байте содержатся 8 бит. Байт можно разделить на 2 части — старший ниббл, который содержит в себе 4 бита, и младший ниббл, тоже 4 бита:
7 6 5 4 | 3 2 1 0 Номер бита
H H H H   L L L L Ниббл
В 16-ричном виде байт к примеру, может выглядеть так 4A или 23. Это значит, что каждый разряд занимает определенный ниббл. Визуально, то есть, именно чисто визуально, можно любое 2х-значное десятичное число представить в виде одного 8-битного, в шестнадцатеричном виде. Отличие между ними большое и прежде всего связано с тем, что процессор работает с набором бит, а десятичная система ему вообще чужеродна, поэтому, когда процессор прибавляет к примеру 28 + 04, то результат будет не 32, а 2C.
Понятное дело, что вот этот результат не похож на 32, и нужно что-то с этим делать, поэтому была придумана и реализована инструкция DAA, которая, как по мне, является самым настоящим костылём в мире программирования на ассемблере.
  • Если после сложения в младшем ниббле результат больше 9, то его необходимо скорректировать, чтобы после 9 шел 0, а не A и что-то другое
  • Если после сложения результат в младшем ниббле от 0 до 9, но при этом был произведен перенос и 3-го в 4-й разряд, то это означает, что и в этом случае результат требуется скорректировать
  • Аналогично, если результат в старшем ниббле более 9, то корректировать надо старший ниббл. По сути коррекция просто представляет из себя +6, поскольку если сложить A + 6 то получим 10h
  • В том числе, если был перенос из 7-го в 8-й бит (то есть CF=1), то также корректировать старший ниббл
  • Существует особенный случай, когда получается число 9A, оно также требует коррекции, поскольку оно по сути своей является результатом сложения 99 + 1. Если выполнить коррекцию только лишь младшего ниббла, прибавив туда +6, то результат окажется неверным и будет равен A0. Видно, что в таком случае надо добавить еще +6 к старшему нибблу, что даст гарантированный перенос CF=1, и в результате получится якобы число 100h.
Все вышесказанное можно описать в верилоге подобным образом:
1wire daa1 = psw[4] || a[3:0] > 9;
2wire daa2 = psw[0] || a[7:4] > 9 || (a[7:4] >= 9 && a[3:0] > 9);
Где если daa1=1, то требуется коррекция младшего ниббла, и где daa2=1, то старшего.
Рассмотрим теперь итоговый код полученной инструкции DAA. Он не настолько сложно выглядит, гораздо сложнее его понять.
18'b0010_0111: case (t)
2
3    0: begin d[15:8] <= (daa1 ? 6 : 0) + (daa2 ? 8'h60 : 0); end
4    1: begin d[ 7:0] <= a + d[15:8]; end
5    2: begin
6
7        psw[SF] <= d[7];
8        psw[ZF] <= d[7:0] == 0;
9        psw[HF] <= a[4] ^ d[4] ^ d[12];
10        psw[PF] <= ~^d[7:0];
11        psw[CF] <= daa2 | psw[0];
12
13        b <= 1;
14        n <= 7;
15
16    end
17    3: begin t <= 0; end
18
19endcase
Алгоритм работы.
  • Такт 0. В старшую часть промежуточного регистра d записывается сумма итоговой коррекции нибблов по рассказанному выше методу
  • Такт 1. В младшую часть записывается скорректированное значение аккумулятора A
  • Такт 2. Выставляются флаги SF, ZF, HF и PF соответственно результату, как и в INR, DCR выше. Конструкция a[4] ^ d[4] ^ d[12] представляет из себя стандартный метод вычисления полупереноса при сложении. В CF возводится 1 тогда, когда daa2=1, то есть, произошла коррекция старшего ниббла. Результаты пишутся (b=1) в регистр A (n=7).
Материалы и коды к статье