Оглавление
- Модификация кода
- Инструкция MVI
- Инструкция SHLD и LDHL
- Инструкция STA, LDA
- Инструкции RLC, RRC, RAL, RAR (сдвиги)
- Инструкции CMA, STC и CMC
- Инструкция DAD
- Инструкции INR, DCR
- Инструкция DAA
§ Модификация кода
Традиционно, перед началом работы внесу определенные улучшающие модификации в код. В предыдущей главе в инструкциях 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
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 предназначен для записи результата в флаги и также в регистр/память.
На этом же такте делается выбор, куда писать. Либо в регистр, либо в память, и соответственно в
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).