Лисья Нора

Оглавление


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

Традиционно, перед началом работы внесу определенные улучшающие модификации в код. В предыдущей главе в инструкциях 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
wire [15:0] cpn = cp + 1;
В общем-то, ничего сложного. Инструкция LDHL почти что идентична:
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
Несмотря на то что инструкции разные, я смог объединить их в одну.
Поэтому, мне не пришлось делать две инструкции, так как легко получилось сделать в одной через вариацию 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.
Соответственно, полученный результат записывается в 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
Алгоритм работы
Из результата инкремента или декремента копируется бит 7-й в SF. Вычисляется ZF=1, если результат получился 0, и PF=1, если количество битов четно. Для HF устанавливается результат =1 в зависимости от того, инкремент это был или декремент. При инкременте HF=1 равен тогда и только тогда, когда младший ниббл оказался равен 0, потому что предыдущее его значение было гарантированно равно F. С декрементом ситуация как раз наоборот, если младший ниббл результата F, то это значит, что был заем из бита 4.
На этом же такте делается выбор, куда писать. Либо в регистр, либо в память, и соответственно в out устанавливается результат, при случае, если будет произведена запись в память.

§ Инструкция 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, которая, как по мне, является самым настоящим костылём в мире программирования на ассемблере.
Все вышесказанное можно описать в верилоге подобным образом:
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
Алгоритм работы.