Оглавление


§ Дополнение пинов

В этой главе потребуется писать и читать в порты, поэтому мне потребуется добавить некоторые новые пины.
1module KR580VM80ALite
2(
3    input               clock,
4    input               reset_n,
5    input               ce,
6    output              m0,
7    output      [15:0]  address,
8    output reg  [ 7:0]  port,         // Добавлено: Номер порта от 0 до 255
9    input       [ 7:0]  in,
10    input       [ 7:0]  port_in,      // Добавлено: Входящие данные от порта
11    output reg  [ 7:0]  out,          // Используется также для записи в порт
12    output reg          we,
13    output reg          port_we,      // Добавлено: Разрешение записи в порт
14    output reg          iff1          // Добавлено: Разрешение или отключение прерываний
15);
В Радио86 не реализованы прерывания как таковые, потому DI/EI инструкции выведены на пищалку и управляют звуком. Ввиду невозможности точно рассчитать такты из-за постоянной остановки процессора видеоконтроллером, звук получается "не очень", и постоянно хрипит.

§ Инструкции RET ccc, RET

Возврат из подпрограммы. С вершины стека берется значение и записывается в PC в том случае, если выполнено условие, выполняется за 11Т. В другом случае, пропуск инструкции, выполняется за 5Т. Это выход из подпрограммы по условию. Всего условий в процессоре предусмотрено восемь: NZ, Z, NC, C, PO, PE, P, M. В самом деле, условий всего лишь 4, просто на каждое дается либо положительный, либо отрицательный ответ.
Например, условие JNZ означает, что выполняться будет тогда, когда ZF=0, условие JZ значит, что выполняться будет только если ZF=1. Всего проверяются флаги: ZF, CF, PF и SF. К примеру, условие "JP" значит что если SF=0, то выполнить переход к метке. Для "JM" означает что если SF=1, то выполнить.
Создам провод на 4 бита, где в каждом бите будет проверяемый флаг.
1wire [3:0] cond = {psw[SF], psw[PF], psw[CF], psw[ZF]};
Здесь бит 0 проверяет флаг ZF, бит 3 — флаг SF. Рассмотрим подробнее код микрооперации.
18'b11xx_x000,
28'b1100_1001: case (t)
3
4    0: begin d <= sp + 2; w <= ccc; n <= 3; sw <= 1; cp <= sp; end
5    1: begin d[ 7:0] <= in; cp <= cpn; end
6    2: begin d[15:8] <= in; sw <= 0; end
7    4: begin t <= ccc ? 5 : 0; end
8    10: begin t <= 0; pc <= d; end
9
10endcase
Здесь объединены два вида RET, условный (11xx_x000) и безусловный опкод (1100_1001), то есть просто RET. Видно, что появился некий провод ccc:
1wire ccc  = (cond[ opc[5:4] ] == opc[3]) || (opc == 8'hC9) || (opc == 8'hCD);
В зависимости от кода условий, которое хранится в op[5:3], будет выбрано решение. Сначала выбираем проверяемый флаг из opc[5:4], через мультиплексор cond[opc[5:4]] смотрим его значение и сравниваем с условием opc[3]. Если совпадает, то ccc=1. Помимо условного перехода, здесь ccc=1 может быть на некоторых инструкциях, это C9=RET и CD=CALL.
Алгоритм.
  • Такт 0. В d записываем новое значение SP+2, сдвиг стека (n=3 это SP), выбираем память cp, указатель на вершину стека и если условие выполняется, то тогда в регистр SP будет записано SP+2, но в противном случае, ничего не будет сделано
  • Такт 1. Читается младший байт из стека (адрес возврата)
  • Такт 2. Читается старший байт из стека
  • Такт 4. Если условие выполнено, то на 10-м такте в регистр PC будет записано прочтенное значение из стека, а иначе просто перейдет к следующей инструкции.

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

Чтение слова из вершины стека. Время исполнения 11Т.
Из стека можно прочесть только в 4 регистра, а именно BC, DE, HL, AF. Последний обрабатывается особым образом. Дело в том, что регистровая пара AF состоит из двух регистров: в старшей части записан аккумулятор A, в младшей регистр PSW, или регистр флагов.
18'b11xx_0001: case (t)
2
3    0: begin d <= sp + 2; w <= 1; n <= 3; sw <= 1; cp <= sp; end
4    1: begin d[ 7:0] <= in; cp <= cpn; end
5    2: begin d[15:8] <= in; sw <= 0; n <= opc[5:4]; w <= opc[5:4] != 3; end
6    3: begin if (opc[5:4] == 3) begin d[7:0] <= d[15:8]; psw <= d[7:0]; b <= 1; n <= 7; end end
7    10: begin t <= 0; end
8
9endcase
Алгоритм.
  • Такт 0. Запись в SP нового значения SP+2, сигналы аналогичны тем же, что и в RET
  • Такт 1. Чтение младшего байта
  • Такт 2. Чтение старшего байта. Отключаем указатель памяти cp (sw=0), и пишется результат в 16-битный регистр (n) только в том случае, если это не AF
  • Такт 3. Если регистровая пара является AF, то в этом случае из старшего байта d переносится в младший для того, чтобы была возможность оттуда записать в регистр A (b=1, n=7), а в PSW записывается то, что было в младшем байте d.
Получается достаточно странно, но этот код работает.

§ Инструкции PCHL, SPHL, DI и EI

Это довольно простые инструкции, поэтому я объединил их в одном параграфе. PCHL записывает HL в программный счетчик PC (PC = HL), SPHL записывает в SP (SP = HL), DI и EI отключают и включают прерывания. Ранее я упоминал, что прерывания в процессоре не реализованы, потому они тут используются для "бипера", генератора звуковых сигналов.
Реализация инструкции PCHL (5 тактов):
18'b1110_1001: case (t)
2    4: begin pc <= hl; t <= 0; end
3endcase
Реализация инструкции SPHL (5 тактов):
18'b1111_1001: case (t)
2    0: begin d <= hl; w <= 1; n <= 3; end
3    4: begin t <= 0; end
4endcase
И реализация инструкции DI и EI (4 такта):
18'b1111_x011: case (t)
2    3: begin t <= 0; iff1 <= opc[3]; end
3endcase
Думаю, что код достаточно очевиден и в данном случае комментарии излишни.

§ Инструкция JMP ccc

Выполняется условный или безусловный переход по абсолютному адресу (16 бит). Вне зависимости, выполнилось ли условие или нет, эта инструкция всегда выполняется за 10 тактов.
18'b11xx_x010,
28'b1100_0011: case (t)
3
4    1: begin cp[ 7:0] <= in; pc <= pcn; end
5    2: begin cp[15:8] <= in; pc <= pcn; end
6    9: begin t <= 0; if (ccc || opc[0]) pc <= cp; end
7
8endcase
Вначале в регистр cp считывается 16-битный адрес, в любом случае он считывается, и на такте #9 выполняется загрузка cp в pc либо по условию, либо безусловно (опкод C3).

§ Инструкции OUT, IN: работа с портами

На самом деле, в Радио86 портов тоже нет, но процессор все-таки умеет с ними работать, так что можно и добавить такую возможность. OUT отправляет в порт значение из аккумулятора, IN же наоборот, читает из порта в аккумулятор. При этом, флаги никак не меняются. Инструкции выполняются за 10Т.
Реализация OUT.
18'b1101_0011: case (t)
2
3    1: begin port <= in; port_we <= 1; out <= a; pc <= pcn; end
4    9: begin t <= 0; end
5
6endcase
Пишется номер порта из непосредственного операнда, а также пишутся данные (out) и устанавливается сигнал port_we=1. Следует обязательно добавить сброс port_we на каждом такте.
1port_we <= 0;
Я этот код занес вне области исполнения микрокодов. С реализацией IN все так же несложно.
18'b1101_1011: case (t)
2
3    1: begin port <= in; pc <= pcn; end
4    2: begin b <= 1; n <= 7; d <= port_in; end
5    9: begin t <= 0; end
6
7endcase
Точно так же устанавливается номер порта, после чего считывается оттуда значение и записывается в регистр A (n=7). Определенного стробирующего такта не отсылается, то есть, предполагаем что при чтении из порта, ничего не защелкивается.

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

Обменивает вершину стека с регистровой парой HL. Выполняется за 18Т. По праву, можно назвать эту инструкцию самой медленно выполняющейся из всех. Она и достаточно сложна и требует 2 чтения и 2 записи в память.
18'b1110_0011: case (t)
2
3    0: begin sw <= 1; cp <= sp; end
4    1: begin d[ 7:0] <= in; cp <= cpn; end
5    2: begin d[15:8] <= in; w <= 1; n <= 2; cp <= hl; end
6    3: begin we <= 1; out <= cp[7:0]; d[7:0] <= cp[15:8]; cp <= sp; end
7    4: begin we <= 1; out <= d[7:0]; cp <= cpn; end
8    17: begin sw <= 0; t <= 0; end
9
10endcase
Алгоритм.
  • Такт 0. Ставится указатель в памяти на вершину стека
  • Такт 1. Читается младший байт из стека
  • Такт 2. Читается старший байт из стека и пишется результат в регистр HL, и старый HL временно сохраняется в CP
  • Такт 3. Пишется в вершину стека младший байт бывшего ранее HL, а старший байт сохраняется временно в регистр d, восстанавливается указатель вершины стека в CP
  • Такт 4. Пишется старший байт в вершину стека
Тут я использовал временно регистр CP для того, чтобы хранить HL, поскольку я не хотел делать новые регистры для этой цели.

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

Обмен регистровых пар DE и HL. Время выполнения — 4Т.
18'b1110_1011: case (t)
2
3    0: begin d <= de; n <= 2; w <= 1; cp <= hl; end
4    1: begin d <= cp; n <= 1; w <= 1; end
5    3: begin t <= 0; end
6
7endcase
Как и в случае с XTHL, я использую для временного хранения регистр CP. На такте #0 в регистр HL пишется значение DE, на такте #1 в регистр DE пишется то, что ранее было в HL.

§ Инструкция CALL ccc

Условный вызов процедуры. Время выполнения либо 11Т, если вызов не произошел, или 17Т, если переход был выполнен. У инструкции есть два варианта исполнения — условный и безусловный. Безусловный вариант имеет опкод CDh и выполняется всегда, то есть в ccc=1, если opc=CDh.
18'b11xx_x100,
28'b1100_1101: case (t)
3
4    1: begin d[ 7:0] <= in; pc <= pcn; end
5    2: begin d[15:8] <= in; pc <= pcn; end
6    3: begin we <= ccc; out <= pc[ 7:0]; sw <= 1; cp <= sp - 2; end
7    4: begin we <= ccc; out <= pc[15:8]; cp <= cpn; end
8    5: begin if (ccc) pc <= d[15:0]; end
9    6: begin w <= ccc; d <= sp - 2; n <= 3; end
10    10: begin t <= ccc ? 11 : 0; end
11    16: begin t <= 0; end
12
13endcase
Алгоритм.
  • Такт 1. Считывание младшего байта адреса перехода
  • Такт 2. Считывание старшего байта адреса перехода
  • Такт 3. Запись в память младшей части PC (или PCL) в стек, но только если условие выполнилось, иначе ничего не пишется
  • Такт 4. Аналогично, запись PCH (старшей части PC) в стек, при выполнении условия
  • Такт 5. Если условие выполнилось, то тогда занести в PC прочитанное значение адреса
  • Такт 6. Запись в регистр SP (n=3) нового значения SP-2, но только если условие выполнено
  • Такт 10. Если переход в процедуру был, то продолжить отсчитывать такты, иначе перейти к следующей инструкции (это вариант выполнения инструкции за 11Т)
В этом микрокоде я пошел на хитрость, записывая или не записывая в регистры и в память новые значения в зависимости от выполнения условия.

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

Запись в стек регистровой пары BC, DE, HL или AF. Выполнение за 11Т.
18'b11xx_0101: case (t)
2
3    0: begin d <= sp - 2; w <= 1; n <= 3; sw <= 1; cp <= sp - 2; end
4    1: begin d <= opc[5:4] == 2'b11 ? {a, (psw & 8'b11010101) | 2'b10} : r16; end
5    2: begin we <= 1; out <= d[ 7:0]; end
6    3: begin we <= 1; out <= d[15:8]; cp <= cpn; end
7    10: begin sw <= 0; t <= 0; end
8
9endcase
Алгоритм.
  • Такт 0. В регистр SP (n=3) пишется (w=1) новое значение SP-2, и также устанавливается адрес памяти тоже CP = SP-2. Туда будут писаться данные.
  • Такт 1. Выбор тех данных которые будут писаться в стек. Если в opc[5:4] указана 3-я регистровая пара, то это AF, иначе регистры BC, DE или HL
  • Такт 2. Запись в память младшего байта регистровой пары
  • Такт 3. Запись в память старшего байта регистровой пары
  • Такт 10. Отключение от указателя памяти CP и переход к исполнению следующей инструкции
Как можно отметить, вместо PSW части я пишу (psw & 8'b11010101) | 2'b10, что позволяет удалить из PSW биты номер 3,5 и установить бит 1 в единицу, поскольку что бы ни было там записано, но в память в этих битах должны писаться именно эти значения.

§ Инструкции АЛУ с непосредственным операндом

Аналогично предыдущим АЛУ инструкциям, они работают с двумя операндами, с операндом А (слева) и непосредственным операндом (справа), который идет сразу за опкодом. Расположение этих инструкции не случайно так сделано, что младшие три бита опкода равны 110b, поскольку позволяет сделать чтение из памяти без всяких дополнительных мультиплексоров.
18'b11xx_x110: case (t)
2
3    1: begin d <= alur; pc <= pcn; b <= (opc[5:3] != CMP); n <= 7; psw <= aluf; end
4    6: begin t <= 0; end
5
6endcase
Вместо выбора из памяти по адресу HL, из памяти теперь выбирается операнд по адресу PC. Соответственно, добавляется PC+1, и результат пишется в регистр A (кроме CMP инструкции), и в PSW регистр.

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

Вызов прерывания, который работает как вызов процедуры по заранее определенному адресу. Например RST 7 вызывает процедуру по адресу 7*8 = 56 в памяти. RST 1 вызывает по адресу 8 и так далее. Всего таких адресов восемь. Выполняется за 11Т.
18'b11xx_x111: case (t)
2
3    1: begin we <= 1; d <= sp - 2; n <= 3; w <= 1; cp <= sp - 2; sw <= 1; out <= pc[7:0]; end
4    2: begin we <= 1; out <= pc[15:8]; cp <= cpn; pc <= {opc[5:3], 3'b000}; end
5    10: begin sw <= 0; t <= 0; end
6
7endcase
Алгоритм.
  • Такт 0. Уменьшение SP=SP-2, выбор CP=SP-2 для записи туда адреса возврата и тут же, сохранение туда младшего байта PCL
  • Такт 1. Сохранение в стек старшего байта PCH и запись в PC вычисленного адреса из номера опкода
И вот на этом весь процессор готов. Сделаны все его инструкции и теперь можно начинать пользоваться. Но это лишь только половина дел, поскольку необходимо сделать к нему видеоадаптер, встроить ОЗУ и ПЗУ, создать симулятор в Verilator. Впереди еще много работы, прежде чем мы увидим какой-то результат.
Материалы к статье. Код процессора