Оглавление
- Модификация кода
- Инструкция MVI
- Инструкция SHLD и LDHL
- Инструкция STA, LDA
- Инструкции RLC, RRC, RAL, RAR (сдвиги)
- Инструкции CMA, STC и CMC
- Инструкция DAD
- Инструкции INR, DCR
- Инструкция DAA
§ Модификация кода
Традиционно, перед началом работы внесу определенные улучшающие модификации в код. В предыдущей главе в инструкциях INX и DCX использовался громоздкий case для выбора регистра. Чтобы избежать лишних строк кода, вынесу выбор 16-битного регистра вне блока always:wire [ 7:0] opc = t ? opcode : in; wire [15:0] r16 = opc[5:4] == 2'b00 ? bc : opc[5:4] == 2'b01 ? de : opc[5:4] == 2'b10 ? hl : sp;Помимо выбора регистра 16-бит, также выбирается и
opc
— текущий опкод, который доступен с любого t. Также для дальнейших целей нам потребуется еще выбор 8-битного регистра точно таким же методом, на диапазоне [5:3]
опкода.wire [ 7:0] op53 = opc[5:3] == 3'b000 ? bc[15:8] : opc[5:3] == 3'b001 ? bc[ 7:0] : opc[5:3] == 3'b010 ? de[15:8] : opc[5:3] == 3'b011 ? de[ 7:0] : opc[5:3] == 3'b100 ? hl[15:8] : opc[5:3] == 3'b101 ? hl[ 7:0] : opc[5:3] == 3'b110 ? in : a;Далее потребуется также выбор регистра на диапазоне
[2:0]
.wire [ 7:0] op20 = opc[2:0] == 3'b000 ? bc[15:8] : opc[2:0] == 3'b001 ? bc[ 7:0] : opc[2:0] == 3'b010 ? de[15:8] : opc[2:0] == 3'b011 ? de[ 7:0] : opc[2:0] == 3'b100 ? hl[15:8] : opc[2:0] == 3'b101 ? hl[ 7:0] : opc[2:0] == 3'b110 ? in : a;Достаточно много мультиплексоров, но в целом, они покрывают все нужны процессора в выборе регистров. Как можно отметить, в выборе источника входа вместо регистра фигурирует
in
, который является выбором источника "память" (M).§ Инструкция MVI
Пересылает байт, идущий за опкодом, либо в регистр (MVI R,*), либо в память (MVI M,*). Указатель памяти — регистровая пара HL. Здесь код можно сделать двумя способами — либо обрабатывать каждый случай отдельно, для регистра и памяти, либо сделать запись в память или регистр совместно.Добавлю еще один полезный проводок в схему.
wire m53 = (opc[5:3] == 3'b110); wire m20 = (opc[2:0] == 3'b110);Это будет необходимо для определения метода адресации. Вместо указания регистра можно указывать адрес в памяти, который является M. Для того, чтобы это знать, у окпода может быть в битах 5:3 либо 110b, либо в битах 2:0, в зависимости от выполняемой инструкции. Для MVI используется диапазон битов 5:3 у опкода.
8'b00xx_x110: case (t) 1: begin pc <= pcn; // PC = PC + 1 cp <= hl; // Указатель HL n <= opc[5:3]; // Номер регистра b <= !m53; // Запись в регистр, если не M we <= m53; // Запись в память, если M sw <= m53; // Активация указателя CP d <= in; // Данные для записи в регистр out <= in; // Данные для записи в память end 6: begin t <= sw ? 7 : 0; sw <= 0; end 9: begin t <= 0; end endcaseПоскольку я объединил запись как в регистр, так и в память, то и количество тактов у них разное. При записи в регистр количество тактов равно 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.
8'b0010_0010: case (t) 1: begin cp[ 7:0] <= in; pc <= pcn; end 2: begin cp[15:8] <= in; pc <= pcn; sw <= 1; end 3: begin we <= 1; out <= hl[ 7:0]; end 4: begin we <= 1; out <= hl[15:8]; cp <= cpn; end 15: begin sw <= 0; t <= 0; end endcase
- На такте 1 и 2 считывается адрес, и на 2-м также этот адрес выбирается вместо PC
- На такте 3 и 4 записываются последовательно запись сначала L регистра в память, а потом H. А также, в CP пишется значение CPN (CP+1)
wire [15:0] cpn = cp + 1;
- На 15-м такте включается опять указатель на PC
8'b0010_1010: case (t) 1: begin cp[ 7:0] <= in; pc <= pcn; end 2: begin cp[15:8] <= in; pc <= pcn; sw <= 1; end 3: begin d [ 7:0] <= in; cp <= cpn; end 4: begin d [15:8] <= in; w <= 1; n <= 2; sw <= 0; end 15: begin t <= 0; end endcaseВначале происходит выборка указателя CP, и далее последовательно читаются данные в 16-битный регистр d и на такте #4 записываются в n=2 (это HL).
§ Инструкция STA, LDA
Эти две инструкции по своей сути разные, STA записывает содержимое A в память по 16-битному адресу, а LDA считывает. Каждая из них выполняется за 13 тактов.8'b0011_x010: case (t) 1: begin cp[ 7:0] <= in; pc <= pcn; end 2: begin cp[15:8] <= in; pc <= pcn; sw <= 1; we <= ~opc[3]; out <= a; end 3: begin d <= in; b <= opc[3]; n <= 7; sw <= 0; end 12: begin t <= 0; end endcaseНесмотря на то что инструкции разные, я смог объединить их в одну.
- Первые два такта (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Т, то я их объединил в один набор.8'b000x_x111: case (t) 0: begin b <= 1; n <= 7; case (opc[4:3]) 2'b00: d <= {a[6:0], a[7]}; // RLC 2'b01: d <= {a[0], a[7:1]}; // RRC 2'b10: d <= {a[6:0], psw[0]}; // RAL 2'b11: d <= {psw[0], a[7:1]}; // RAR endcase psw[0] <= a[opc[3] ? 0 : 7]; end 3: begin t <= 0; end endcaseВ зависимости от того, какая инструкция, будет записан флаг 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:8'b0010_1111: case (t) 0: begin d <= ~a; b <= 1; n <= 7; end 3: begin t <= 0; end endcaseРеализация STC и CMC.
8'b0011_x111: case (t) 0: begin psw[0] <= opc[3] ? /*CMC*/ ~psw[0] : /*STC*/ 1'b1; end 3: begin t <= 0; end endcaseКомментарии мне кажется, излишни тут.
§ Инструкция DAD
Сложение регистровой пары HL с BC, DE, HL или SP и запись в HL результата. При переполнении 16-битного результата устанавливается CF=1. Инструкция выполняется 10 тактов.8'b00xx_1001: case (t) 0: begin d <= hl + r16; w <= 1; n <= 2; end 1: begin psw[0] <= d[16]; end 9: begin t <= 0; end endcaseНа #0 такте происходит как сложение результата, так и выбирается запись (w=1) в регистр HL (n=2). На следующем такте записывается 16-й бит переноса во флаг CF, или psw[0].
§ Инструкции INR, DCR
Эти две инструкции представляют из себя инкремент (INR) и декремент (DCR) регистра или 8-битного значения в памяти. Есть возможность уместить в одном коде также выборку из памяти, и также из регистров. Ниже приведен код, и далее разберу подробнее, как он работает.8'b00xx_x10x: case (t) 0: begin cp <= hl; sw <= 1; end 1: begin d <= opc[0] ? op53 - 1 : op53 + 1; end 2: begin psw[SF] <= d[7]; psw[ZF] <= d[7:0] == 0; psw[HF] <= d[3:0] == (opc[0] ? 4'hF : 4'h0); psw[PF] <= ~^d[7:0]; n <= opc[5:3]; b <= ~m53; // Либо в регистр запись we <= m53; // Либо запись в память out <= d; end 4: begin sw <= 0; t <= m53 ? 5 : 0; end 9: begin t <= 0; end endcaseАлгоритм работы
- Такт 0 устанавливает указатель адреса в памяти на HL (указываем sw=1). Это потребуется для того случая, если нужно будет прочесть значение из памяти
- Такт 1 непосредственно вычисляет либо +1, либо -1, в зависимости от опкода [INR или DCR]
- Такт 2 предназначен для записи результата в флаги и также в регистр/память.
На этом же такте делается выбор, куда писать. Либо в регистр, либо в память, и соответственно в
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
.
wire daa1 = psw[4] || a[3:0] > 9; wire daa2 = psw[0] || a[7:4] > 9 || (a[7:4] >= 9 && a[3:0] > 9);Где если daa1=1, то требуется коррекция младшего ниббла, и где daa2=1, то старшего.
Рассмотрим теперь итоговый код полученной инструкции DAA. Он не настолько сложно выглядит, гораздо сложнее его понять.
8'b0010_0111: case (t) 0: begin d[15:8] <= (daa1 ? 6 : 0) + (daa2 ? 8'h60 : 0); end 1: begin d[ 7:0] <= a + d[15:8]; end 2: begin psw[SF] <= d[7]; psw[ZF] <= d[7:0] == 0; psw[HF] <= a[4] ^ d[4] ^ d[12]; psw[PF] <= ~^d[7:0]; psw[CF] <= daa2 | psw[0]; b <= 1; n <= 7; end 3: begin t <= 0; end endcaseАлгоритм работы.
- Такт 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).